diff --git a/composer.json b/composer.json index 7c83bb2..111e7a1 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "require-dev": { "behat/behat": "^3.6.1", - "behat/mink-selenium2-driver": "^1.4", + "behat/mink-selenium2-driver": "~1.6.0", "bitbag/coding-standard": "^3.0", "dmore/behat-chrome-extension": "^1.3", "dmore/chrome-mink-driver": "^2.7", diff --git a/features/caching_sulu_request.feature b/features/caching_sulu_request.feature new file mode 100644 index 0000000..4cc118d --- /dev/null +++ b/features/caching_sulu_request.feature @@ -0,0 +1,50 @@ +@sulu_cache +Feature: Caching sulu page request + In order to see again some page + As a Visitor + I want to be able to see the page without sending request to sulu + + Background: + Given the store operates on a single channel in "United States" + And Sulu has defined page "blog_page_with_blocks_and_links" in locale "en_US" + And Cache for sulu not exists + + + @ui + Scenario: See the featured pages on homepage + When I visit this channel's homepage + And "United States" has enabled sulu localized requests + Then Sulu cache should not exists + And I visit a sulu page "blog_page_with_blocks_and_links" in locale en_US + And Sulu cache should exists for "United States" with locale en_US + + @ui + Scenario: Manually clear cache when localized urls are disabled + When I visit a sulu page "blog_page_with_blocks_and_links" in locale en_US + Then Sulu cache should exists + And I am logged in as an administrator + And I browse channels + And I should see button "Purge sulu cache" in "United States" + And I click "Clear Cache" button in "United States" + And I should see success "Successfully purged" flash message + And Sulu cache should exists for "United States" with locale en_US + + @ui + Scenario: Manually clear cache when localized urls are enabled + When "United States" has enabled sulu localized requests + And I visit a sulu page "blog_page_with_blocks_and_links" in locale en_US + Then Sulu cache should exists + And I am logged in as an administrator + And I browse channels + And I should see expanded button "Purge sulu cache" in "United States" + And I click expanded "en_US" button in "United States" + And I should see success "Successfully purged" flash message + And Sulu cache should exists for "United States" with locale en_US + + @ui + Scenario: Manually clear cache when localized urls are disabled and cache not exists + When I am logged in as an administrator + And I browse channels + Then I should see button "Purge sulu cache" in "United States" + And I click "Purge sulu cache" button in "United States" + And I should see error "Dir with sulu cache not found" flash message diff --git a/features/render_sulu_page.feature b/features/render_sulu_page.feature new file mode 100644 index 0000000..b617487 --- /dev/null +++ b/features/render_sulu_page.feature @@ -0,0 +1,27 @@ +@render_sulu_page +Feature: Render sulu page + In order to see a sulu page + As a Visitor + I want to be able to see rendered sulu page + + Background: + Given the store operates on a single channel in "United States" + And Sulu has defined page "blog_page_with_properties" in locale "en_US" + And Sulu has defined page "blog_page_with_blocks_and_links" in locale "en_US" + And Page "blog_page_with_blocks_and_links" has block "quote" + And Page "blog_page_with_blocks_and_links" has block "text" + And Page "blog_page_with_blocks_and_links" has block "image" + + @ui + Scenario: Rendering a sulu page with properties + When I visit a sulu page "blog_page_with_properties" in locale en_US + Then I should see a "title" with value "E-commerce trends" + And I should see a "content" with value "CONTENT" + + @ui + Scenario: Rendering a sulu page with properties and blocks + When I visit a sulu page "blog_page_with_blocks_and_links" in locale en_US + Then I should see a "title" with value "E-commerce trends" + And I should see a block "content" with value "2021 was followed by the time of the 2020 pandemic. During these two years, a lot has changed..." + And I should see a block image with url "https://en.wikipedia.org/wiki/Cat#/media/File:Sheba1.JPG" + And I should see a block "quote" with value "Lorem ipsum dolor sit amet, con" diff --git a/features/show_featured_pages_on_homepage.feature b/features/show_featured_pages_on_homepage.feature new file mode 100644 index 0000000..daa2964 --- /dev/null +++ b/features/show_featured_pages_on_homepage.feature @@ -0,0 +1,20 @@ +@sulu_page +Feature: Show featured pages on homepage + In order to see featured pages on homepage + As a Visitor + I want to be able to see featured pages on homepage + + Background: + Given the store operates on a single channel in "United States" + And Sulu has defined featured pages list featured_pages in locale en_US + And One of the featured page is page "Blog Page 1" + And One of the featured page is page "Blog Page 2" + And One of the featured page is page "Blog Page 3" + + @ui + Scenario: See the featured pages on homepage + When I visit this channel's homepage + Then I should see 3 featured pages. + And I should see featured page "Blog Page 1" + And I should see featured page "Blog Page 2" + And I should see featured page "Blog Page 3" diff --git a/spec/ApiClient/SuluApiClientSpec.php b/spec/ApiClient/SuluApiClientSpec.php new file mode 100644 index 0000000..96d6f07 --- /dev/null +++ b/spec/ApiClient/SuluApiClientSpec.php @@ -0,0 +1,68 @@ +beConstructedWith($client, $shopperContext, $suluBaseUri, $cacheDir); + } + + public function it_is_initializable(): void + { + $this->shouldHaveType(SuluApiClient::class); + } + + public function it_implements_sulu_api_client_interface_interface(): void + { + $this->shouldHaveType(SuluApiClientInterface::class); + } + +// function it_fetches_cms_content( +// HttpClientInterface $client, +// ShopperContextInterface $shopperContext, +// ChannelInterface $channel, +// ) { +// $url = '/cms/content'; +// $locale = 'en_US'; +// +// $shopperContext->getChannel()->willReturn($channel); +// $shopperContext->getLocaleCode()->willReturn($locale); +// $channel->isSuluUseLocalizedUrls()->willReturn(true); +// +// $client->request('GET', 'http://example.com/en_US/cms/content.json', ['headers' => ['Accept' => 'application/json']])->shouldBeCalled(); +// +// $this->fetchCmsContent($url); +// } +// +// function it_fetches_cms_content_with_global_channel( +// HttpClientInterface $client, +// ShopperContextInterface $shopperContext, +// ChannelInterface $channel, +// ) { +// $url = '/cms/content'; +// +// $shopperContext->getChannel()->willReturn($channel); +// $shopperContext->getLocaleCode()->willReturn(null); +// $channel->isSuluUseLocalizedUrls()->willReturn(false); +// +// +// $client->request('GET', 'http://example.com/cms/content.json', ['headers' => ['Accept' => 'application/json']])->shouldBeCalled(); +// +// $this->fetchCmsContent($url); +// } +} diff --git a/spec/Controller/Action/PurgeSuluCacheActionSpec.php b/spec/Controller/Action/PurgeSuluCacheActionSpec.php new file mode 100644 index 0000000..465c477 --- /dev/null +++ b/spec/Controller/Action/PurgeSuluCacheActionSpec.php @@ -0,0 +1,62 @@ +beConstructedWith($channelRepository, $requestStack, $translator, $cacheDir); + } + + function it_is_initializable() + { + $this->shouldHaveType(PurgeSuluCacheAction::class); + } + + function it_returns_response( + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channel, + RequestStack $requestStack, + Request $request, + ServerBag $serverBag, + Session $session, + FlashBagInterface $flashbag, + ) { + $channelId = 1; + $localeCode = 'en_US'; + $referer = 'http://example.com'; + + $request->get('id')->willReturn($channelId); + $request->get('locale')->willReturn($localeCode); + $request->server = $serverBag; + $serverBag->get('HTTP_REFERER')->willReturn($referer); + $session->getFlashBag()->willReturn($flashbag); + + $channel->getCode()->willReturn('TEST'); + $channelRepository->find($channelId)->willReturn($channel); + + $requestStack->getSession()->willReturn($session); + + $this->__invoke($request)->shouldBeAnInstanceOf(RedirectResponse::class); + } +} diff --git a/spec/Controller/Action/RenderPageActionSpec.php b/spec/Controller/Action/RenderPageActionSpec.php new file mode 100644 index 0000000..abadab0 --- /dev/null +++ b/spec/Controller/Action/RenderPageActionSpec.php @@ -0,0 +1,84 @@ +beConstructedWith($suluApiClient, $pageRendererStrategy); + } + + function it_is_initializable() + { + $this->shouldHaveType(RenderPageAction::class); + } + + function it_returns_response_for_valid_url( + SuluApiClientInterface $suluApiClient, + SuluPageRendererStrategyInterface $pageRendererStrategy, + Request $request, + ) { + $url = 'example-page'; + $pageContent = '

Example Page

'; + + $request->get('slug')->willReturn($url); + $request->get('second_slug')->willReturn(null); + + $suluApiClient->fetchCmsContent($url)->willReturn(['content' => $pageContent]); + $pageRendererStrategy->renderPage(['content' => $pageContent])->willReturn($pageContent); + + $this->__invoke($request)->shouldBeAnInstanceOf(Response::class); + $this->__invoke($request)->getContent()->shouldReturn($pageContent); + } + + function it_returns_response_for_valid_url_with_second_slug( + SuluApiClientInterface $suluApiClient, + SuluPageRendererStrategyInterface $pageRendererStrategy, + Request $request, + ) { + $url = 'example-page'; + $secondSlug = 'second-slug'; + $fullUrl = "{$url}/{$secondSlug}"; + $pageContent = '

Example Page

'; + + $request->get('slug')->willReturn($url); + $request->get('second_slug')->willReturn($secondSlug); + + $suluApiClient->fetchCmsContent($fullUrl)->willReturn(['content' => $pageContent]); + $pageRendererStrategy->renderPage(['content' => $pageContent])->willReturn($pageContent); + + $this->__invoke($request)->shouldBeAnInstanceOf(Response::class); + $this->__invoke($request)->getContent()->shouldReturn($pageContent); + } + + function it_returns_404_response_for_unknown_url( + SuluApiClientInterface $suluApiClient, + Request $request, + SuluPageRendererStrategyInterface $pageRendererStrategy, + ) { + $url = 'non-existing-page'; + + $request->get('slug')->willReturn($url); + $request->get('second_slug')->willReturn(null); + + $suluApiClient->fetchCmsContent($url)->willReturn([]); + $pageRendererStrategy->renderPage([])->willReturn(''); + + $response = $this->__invoke($request); + $response->shouldBeAnInstanceOf(Response::class); + $response->getStatusCode()->shouldReturn(404); + } +} diff --git a/spec/Renderer/Block/SuluBlockRendererStrategySpec.php b/spec/Renderer/Block/SuluBlockRendererStrategySpec.php new file mode 100644 index 0000000..9bf9480 --- /dev/null +++ b/spec/Renderer/Block/SuluBlockRendererStrategySpec.php @@ -0,0 +1,63 @@ +beConstructedWith([$blockRenderer1, $blockRenderer2]); + } + + function it_is_initializable() + { + $this->shouldHaveType(SuluBlockRendererStrategy::class); + } + + function it_throws_runtime_error_if_no_renderer_is_found( + SuluBlockRenderStrategyInterface $blockRenderer1, + SuluBlockRenderStrategyInterface $blockRenderer2, + ) { + $blockData = ['type' => 'non-existing-type']; + + $blockRenderer1->support($blockData)->willReturn(false); + $blockRenderer2->support($blockData)->willReturn(false); + + $this->shouldThrow(RuntimeError::class)->during('renderBlock', [$blockData]); + } + + function it_returns_rendered_block_content( + SuluBlockRenderStrategyInterface $blockRenderer1, + SuluBlockRenderStrategyInterface $blockRenderer2, + ) { + $blockData = ['type' => 'existing-type', 'data' => []]; + $renderedContent = '
Rendered content
'; + + $blockRenderer1->support($blockData)->willReturn(false); + $blockRenderer2->support($blockData)->willReturn(true); + $blockRenderer2->render($blockData)->willReturn($renderedContent); + + $this->renderBlock($blockData)->shouldReturn($renderedContent); + } + + function it_returns_empty_string_on_render_error( + SuluBlockRenderStrategyInterface $blockRenderer1, + SuluBlockRenderStrategyInterface $blockRenderer2, + ) { + $blockData = ['type' => 'existing-type', 'data' => []]; + + $blockRenderer1->support($blockData)->willReturn(false); + $blockRenderer2->support($blockData)->willReturn(true); + $blockRenderer2->render($blockData)->willThrow(Error::class); + + $this->renderBlock($blockData)->shouldReturn(''); + } +} diff --git a/spec/Renderer/Page/SuluPageRendererStrategySpec.php b/spec/Renderer/Page/SuluPageRendererStrategySpec.php new file mode 100644 index 0000000..5442595 --- /dev/null +++ b/spec/Renderer/Page/SuluPageRendererStrategySpec.php @@ -0,0 +1,64 @@ +beConstructedWith([$pageRenderer1, $pageRenderer2]); + } + + function it_is_initializable() + { + $this->shouldHaveType(SuluPageRendererStrategy::class); + } + + function it_throws_runtime_error_if_no_renderer_is_found( + SuluPageRenderStrategyInterface $pageRenderer1, + SuluPageRenderStrategyInterface $pageRenderer2, + ) { + $page = ['template' => 'non-existing-template']; + + $pageRenderer1->support($page)->willReturn(false); + $pageRenderer2->support($page)->willReturn(false); + + $this->shouldThrow(\Twig\Error\RuntimeError::class)->during('renderPage', [$page]); + } + + function it_returns_rendered_page_content( + SuluPageRenderStrategyInterface $pageRenderer1, + SuluPageRenderStrategyInterface $pageRenderer2, + ) { + $page = ['template' => 'existing-template', 'data' => []]; + $renderedContent = '

Rendered Page

'; + + $pageRenderer1->support($page)->willReturn(false); + $pageRenderer2->support($page)->willReturn(true); + $pageRenderer2->render($page)->willReturn($renderedContent); + + $this->renderPage($page)->shouldReturn($renderedContent); + } + + function it_returns_empty_string_on_render_error( + SuluPageRenderStrategyInterface $pageRenderer1, + SuluPageRenderStrategyInterface $pageRenderer2, + ) { + $page = ['template' => 'existing-template', 'data' => []]; + + $pageRenderer1->support($page)->willReturn(false); + $pageRenderer2->support($page)->willReturn(true); + $pageRenderer2->render($page)->willThrow(Error::class); + + $this->renderPage($page)->shouldReturn(''); + } +} diff --git a/spec/Twig/Runtime/SuluRuntimeSpec.php b/spec/Twig/Runtime/SuluRuntimeSpec.php new file mode 100644 index 0000000..41247a0 --- /dev/null +++ b/spec/Twig/Runtime/SuluRuntimeSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($suluApiClient, $blockRendererStrategy, $pageRendererStrategy); + } + + function it_is_initializable() + { + $this->shouldHaveType(SuluRuntime::class); + } + + function it_fetches_sulu_page_from_api_client(SuluApiClientInterface $suluApiClient) + { + $id = 'example-page-id'; + $page = ['id' => $id, 'title' => 'Example Page']; + + $suluApiClient->fetchCmsContent($id)->willReturn($page); + + $this->getSuluPage($id)->shouldReturn($page); + } + + function it_renders_sulu_page_using_page_renderer_strategy( + SuluApiClientInterface $suluApiClient, + SuluPageRendererStrategyInterface $pageRendererStrategy, + ) { + $id = 'example-page-id'; + $page = ['id' => $id, 'title' => 'Example Page']; + $renderedPage = '

Example Page

'; + + $suluApiClient->fetchCmsContent($id)->willReturn($page); + $pageRendererStrategy->renderPage($page)->willReturn($renderedPage); + + $this->renderSuluPage($id)->shouldReturn($renderedPage); + } + + function it_renders_sulu_blocks_using_block_renderer_strategy( + SuluBlockRendererStrategyInterface $blockRendererStrategy, + ) { + $blocks = [['type' => 'block-type-1'], ['type' => 'block-type-2']]; + $renderedBlocks = '
Block 1

Block 2

'; + $divider = '
'; + + $blockRendererStrategy->renderBlock($blocks[0])->willReturn('
Block 1
'); + $blockRendererStrategy->renderBlock($blocks[1])->willReturn('
Block 2
'); + + $this->renderSuluBlocks($blocks, $divider)->shouldReturn($renderedBlocks); + } + + function it_renders_sulu_blocks_with_specified_type_using_block_renderer_strategy( + SuluBlockRendererStrategyInterface $blockRendererStrategy, + ) { + $blocks = [['type' => 'block-type-1'], ['type' => 'block-type-2'], ['type' => 'block-type-1']]; + $filteredBlocks = [['type' => 'block-type-1'], ['type' => 'block-type-1']]; + $renderedBlocks = '
Block 1

Block 1

'; + $divider = '
'; + + $blockRendererStrategy->renderBlock($filteredBlocks[0])->willReturn('
Block 1
'); + $blockRendererStrategy->renderBlock($filteredBlocks[1])->willReturn('
Block 1
'); + + $this->renderSuluBlocksWithType($blocks, 'block-type-1', $divider)->shouldReturn($renderedBlocks); + } + + function it_renders_first_sulu_block_with_specified_type_using_block_renderer_strategy( + SuluBlockRendererStrategyInterface $blockRendererStrategy, + ) { + $blocks = [['type' => 'block-type-1', 'content' => 'Block 1 content'], ['type' => 'block-type-2']]; + $filteredBlocks = [['type' => 'block-type-1', 'content' => 'Block 1 content']]; + $renderedBlock = '
Block 1
'; + + $blockRendererStrategy->renderBlock($filteredBlocks[0])->willReturn('
Block 1
'); + + $this->renderSuluBlockWithType($blocks, 'block-type-1')->shouldReturn($renderedBlock); + } +} diff --git a/src/ApiClient/SuluApiClient.php b/src/ApiClient/SuluApiClient.php index d98694d..e345cdf 100644 --- a/src/ApiClient/SuluApiClient.php +++ b/src/ApiClient/SuluApiClient.php @@ -12,7 +12,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface; use Webmozart\Assert\Assert; -final class SuluApiClient +final class SuluApiClient implements SuluApiClientInterface { public function __construct( private HttpClientInterface $client, diff --git a/src/ApiClient/SuluApiClientInterface.php b/src/ApiClient/SuluApiClientInterface.php new file mode 100644 index 0000000..2a8553e --- /dev/null +++ b/src/ApiClient/SuluApiClientInterface.php @@ -0,0 +1,10 @@ +pageRendererStrategy->renderPage($page); if (strlen($page) === 0) { - return new Response(null, 404); + return new Response('', 404); } return new Response($page); diff --git a/src/Entity/ChannelInterface.php b/src/Entity/ChannelInterface.php index 7a5e04f..135c82d 100644 --- a/src/Entity/ChannelInterface.php +++ b/src/Entity/ChannelInterface.php @@ -4,7 +4,9 @@ namespace BitBag\SyliusSuluPlugin\Entity; -interface ChannelInterface +use Sylius\Component\Core\Model\ChannelInterface as BaseChannelInterface; + +interface ChannelInterface extends BaseChannelInterface { public function isSuluUseLocalizedUrls(): bool; diff --git a/src/Entity/SuluChannelConfigurationTrait.php b/src/Entity/SuluChannelConfigurationTrait.php index 2143228..583e729 100644 --- a/src/Entity/SuluChannelConfigurationTrait.php +++ b/src/Entity/SuluChannelConfigurationTrait.php @@ -4,8 +4,18 @@ namespace BitBag\SyliusSuluPlugin\Entity; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * + * @ORM\Table(name="sylius_channel") + */ +#[ORM\Entity] +#[ORM\Table(name: 'sylius_channel')] trait SuluChannelConfigurationTrait { + /** @ORM\Column(type="boolean", nullable=false) */ protected bool $suluUseLocalizedUrls = false; public function isSuluUseLocalizedUrls(): bool diff --git a/src/Renderer/Block/SuluBlockRendererStrategy.php b/src/Renderer/Block/SuluBlockRendererStrategy.php index 79ab6ae..421fa40 100644 --- a/src/Renderer/Block/SuluBlockRendererStrategy.php +++ b/src/Renderer/Block/SuluBlockRendererStrategy.php @@ -7,7 +7,7 @@ use Twig\Error\Error; use Twig\Error\RuntimeError; -final class SuluBlockRendererStrategy +final class SuluBlockRendererStrategy implements SuluBlockRendererStrategyInterface { public function __construct( private iterable $blockRenderers, diff --git a/src/Renderer/Block/SuluBlockRendererStrategyInterface.php b/src/Renderer/Block/SuluBlockRendererStrategyInterface.php new file mode 100644 index 0000000..80940e3 --- /dev/null +++ b/src/Renderer/Block/SuluBlockRendererStrategyInterface.php @@ -0,0 +1,10 @@ +suluRuntime, 'renderSuluBlocks']), new TwigFunction('bitbag_render_sulu_blocks_with_type', [$this->suluRuntime, 'renderSuluBlocksWithType']), new TwigFunction('bitbag_render_sulu_block_with_type', [$this->suluRuntime, 'renderSuluBlockWithType']), + new TwigFunction('bitbag_page_has_sulu_block', [$this->suluRuntime, 'hasSuluBlock']), ]; } } diff --git a/src/Twig/Runtime/SuluRuntime.php b/src/Twig/Runtime/SuluRuntime.php index 7451ba9..f31e6ea 100644 --- a/src/Twig/Runtime/SuluRuntime.php +++ b/src/Twig/Runtime/SuluRuntime.php @@ -4,16 +4,16 @@ namespace BitBag\SyliusSuluPlugin\Twig\Runtime; -use BitBag\SyliusSuluPlugin\ApiClient\SuluApiClient; -use BitBag\SyliusSuluPlugin\Renderer\Block\SuluBlockRendererStrategy; -use BitBag\SyliusSuluPlugin\Renderer\Page\SuluPageRendererStrategy; +use BitBag\SyliusSuluPlugin\ApiClient\SuluApiClientInterface; +use BitBag\SyliusSuluPlugin\Renderer\Block\SuluBlockRendererStrategyInterface; +use BitBag\SyliusSuluPlugin\Renderer\Page\SuluPageRendererStrategyInterface; class SuluRuntime implements SuluRuntimeInterface { public function __construct( - private SuluApiClient $suluApiClient, - private SuluBlockRendererStrategy $blockRendererStrategy, - private SuluPageRendererStrategy $pageRendererStrategy, + private SuluApiClientInterface $suluApiClient, + private SuluBlockRendererStrategyInterface $blockRendererStrategy, + private SuluPageRendererStrategyInterface $pageRendererStrategy, ) { } @@ -59,10 +59,10 @@ public function renderSuluBlocksWithType(array $blocks, string $type, ?string $d public function renderSuluBlockWithType(array $blocks, string $type): string { $blocks = array_filter($blocks, fn (array $block) => $block['type'] === $type); - if (count($blocks) > 1) { $blocks = $blocks[0]; } + $content = ''; foreach ($blocks as $block) { @@ -71,4 +71,21 @@ public function renderSuluBlockWithType(array $blocks, string $type): string return $content; } + + public function hasSuluBlock(array $page, string $type): bool + { + if (!array_key_exists('blocks', $page)) { + return false; + } + + $blocks = $page['blocks']; + + if (count($blocks) === 0) { + return false; + } + + $blocks = array_filter($blocks, fn (array $block) => $block['type'] === $type); + + return count($blocks) !== 0; + } } diff --git a/src/Twig/Runtime/SuluRuntimeInterface.php b/src/Twig/Runtime/SuluRuntimeInterface.php index 643f1ec..f1c7753 100644 --- a/src/Twig/Runtime/SuluRuntimeInterface.php +++ b/src/Twig/Runtime/SuluRuntimeInterface.php @@ -17,4 +17,6 @@ public function renderSuluBlocks(array $blocks): string; public function renderSuluBlocksWithType(array $blocks, string $type): string; public function renderSuluBlockWithType(array $blocks, string $type): string; + + public function hasSuluBlock(array $page, string $type): bool; } diff --git a/templates/Admin/Grid/Action/invalidateSuluCache.html.twig b/templates/Admin/Grid/Action/invalidateSuluCache.html.twig index 2523db1..24be26c 100644 --- a/templates/Admin/Grid/Action/invalidateSuluCache.html.twig +++ b/templates/Admin/Grid/Action/invalidateSuluCache.html.twig @@ -1,5 +1,5 @@ {% if true == data.isSuluUseLocalizedUrls %} -