Skip to content

Commit

Permalink
Use HttpClient instead of file_get_contents(), add tests and document…
Browse files Browse the repository at this point in the history
…ation
  • Loading branch information
Kocal committed Oct 1, 2024
1 parent 2fe9a88 commit f5b9a1d
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 12 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"require-dev": {
"symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/http-client": "^5.4 || ^6.2 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0",
"symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/web-link": "^5.4 || ^6.2 || ^7.0"
Expand Down
2 changes: 2 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ file:
# if you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# or if you use a CDN:
# frontend: 'https://cdn.example.com/frontend/build'
# pass the build name" as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
Expand Down
9 changes: 7 additions & 2 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.13.1@086b94371304750d1c673315321a55d15fc59015">
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
<file src="src/Asset/EntrypointLookup.php">
<InvalidReturnStatement>
<code><![CDATA[$this->entriesData]]></code>
</InvalidReturnStatement>
</file>
<file src="src/DependencyInjection/Configuration.php">
<UndefinedMethod>
<code>children</code>
<code><![CDATA[children]]></code>
</UndefinedMethod>
</file>
</files>
32 changes: 26 additions & 6 deletions src/Asset/EntrypointLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace Symfony\WebpackEncoreBundle\Asset;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\WebpackEncoreBundle\Exception\EntrypointNotFoundException;

/**
Expand All @@ -35,12 +37,15 @@ class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProvid

private $strictMode;

public function __construct(string $entrypointJsonPath, ?CacheItemPoolInterface $cache = null, ?string $cacheKey = null, bool $strictMode = true)
private $httpClient;

public function __construct(string $entrypointJsonPath, ?CacheItemPoolInterface $cache = null, ?string $cacheKey = null, bool $strictMode = true, ?HttpClientInterface $httpClient = null)
{
$this->entrypointJsonPath = $entrypointJsonPath;
$this->cache = $cache;
$this->cacheKey = $cacheKey;
$this->strictMode = $strictMode;
$this->httpClient = $httpClient;
}

public function getJavaScriptFiles(string $entryName): array
Expand Down Expand Up @@ -119,16 +124,31 @@ private function getEntriesData(): array
}
}

$entrypointJsonContents = file_get_contents($this->entrypointJsonPath);
if ($entrypointJsonContents === false) {
if (str_starts_with($this->entrypointJsonPath, 'http')) {
if (null === $this->httpClient && !class_exists(HttpClient::class)) {
throw new \LogicException(\sprintf('You cannot fetch the entrypoints file from URL "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', $this->entrypointJsonPath));
}
$httpClient = $this->httpClient ?? HttpClient::create();

$response = $httpClient->request('GET', $this->entrypointJsonPath);

if (200 !== $response->getStatusCode()) {
if (!$this->strictMode) {
return [];
}
throw new \InvalidArgumentException(\sprintf('Could not find the entrypoints file from URL "%s": the HTTP request failed with status code %d.', $this->entrypointJsonPath, $response->getStatusCode()));
}

$this->entriesData = $response->toArray();
} elseif (!file_exists($this->entrypointJsonPath)) {
if (!$this->strictMode) {
return [];
}
throw new \InvalidArgumentException(\sprintf('Could not find the entrypoints file from Webpack: the file "%s" does not exist or it is not readable.', $this->entrypointJsonPath));
throw new \InvalidArgumentException(\sprintf('Could not find the entrypoints file from Webpack: the file "%s" does not exist.', $this->entrypointJsonPath));
} else {
$this->entriesData = json_decode(file_get_contents($this->entrypointJsonPath), true);
}

$this->entriesData = json_decode($entrypointJsonContents, true);

if (null === $this->entriesData) {
throw new \InvalidArgumentException(\sprintf('There was a problem JSON decoding the "%s" file', $this->entrypointJsonPath));
}
Expand Down
9 changes: 6 additions & 3 deletions src/CacheWarmer/EntrypointCacheWarmer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,31 @@

use Symfony\Bundle\FrameworkBundle\CacheWarmer\AbstractPhpFileCacheWarmer;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup;
use Symfony\WebpackEncoreBundle\Exception\EntrypointNotFoundException;

class EntrypointCacheWarmer extends AbstractPhpFileCacheWarmer
{
private $cacheKeys;
private $httpClient;

public function __construct(array $cacheKeys, string $phpArrayFile)
public function __construct(array $cacheKeys, ?HttpClientInterface $httpClient, string $phpArrayFile)
{
$this->cacheKeys = $cacheKeys;
$this->httpClient = $httpClient;
parent::__construct($phpArrayFile);
}

protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool
{
foreach ($this->cacheKeys as $cacheKey => $path) {
// If the file does not exist then just skip past this entry point.
if (!file_exists($path)) {
if (!str_starts_with($path, 'http') && !file_exists($path)) {
continue;
}

$entryPointLookup = new EntrypointLookup($path, $arrayAdapter, $cacheKey);
$entryPointLookup = new EntrypointLookup($path, $arrayAdapter, $cacheKey, httpClient: $this->httpClient);

try {
$entryPointLookup->getJavaScriptFiles('dummy');
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/WebpackEncoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ private function entrypointFactory(ContainerBuilder $container, string $name, st
$cacheEnabled ? new Reference('webpack_encore.cache') : null,
$name,
$strictMode,
new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE),
];
$definition = new Definition(EntrypointLookup::class, $arguments);
$definition->addTag('kernel.reset', ['method' => 'reset']);
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<service id="webpack_encore.entrypoint_lookup.cache_warmer" class="Symfony\WebpackEncoreBundle\CacheWarmer\EntrypointCacheWarmer">
<tag name="kernel.cache_warmer" />
<argument /> <!-- build list of entrypoint paths -->
<argument type="service" id="http_client" on-invalid="null" />
<argument>%kernel.cache_dir%/webpack_encore.cache.php</argument>
</service>

Expand Down Expand Up @@ -67,5 +68,7 @@
<tag name="kernel.event_subscriber" />
<argument type="service" id="webpack_encore.entrypoint_lookup_collection" />
</service>

<service id="webpack_encore.http_client" alias="http_client" />
</services>
</container>
99 changes: 98 additions & 1 deletion tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\Log\Logger;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
Expand Down Expand Up @@ -71,6 +73,26 @@ public function testTwigIntegration()
'<script src="/build/other4.js"></script>',
$html2
);

$html3 = $twig->render('@integration_test/template_remote.twig');
$this->assertStringContainsString(
'<script src="https://cdn.example.com/app.js?v=abcde01" referrerpolicy="origin"></script>',
$html3
);
$this->assertStringContainsString(
'<link rel="stylesheet" href="https://cdn.example.com/app.css?v=abcde02">',
$html3
);

$html4 = $twig->render('@integration_test/manual_template_remote.twig');
$this->assertStringContainsString(
'<script src="https://cdn.example.com/backend.js?v=abcde01"></script>',
$html4
);
$this->assertStringContainsString(
'<link rel="stylesheet" href="https://cdn.example.com/backend.css?v=abcde02" />',
$html4
);
}

public function testEntriesAreNotRepeatedWhenAlreadyOutputIntegration()
Expand Down Expand Up @@ -137,7 +159,7 @@ public function testCacheWarmer()
$this->assertFileExists($cachePath);
$data = require $cachePath;
// check for both build keys
$this->assertSame(['_default', 'different_build'], array_keys($data[0] ?? $data));
$this->assertSame(['_default', 'different_build', 'remote_build'], array_keys($data[0] ?? $data));
}

public function testEnabledStrictModeThrowsExceptionIfBuildMissing()
Expand Down Expand Up @@ -165,6 +187,31 @@ public function testDisabledStrictModeIgnoresMissingBuild()
self::assertSame('', trim($html));
}

public function testEnabledStrictModeThrowsExceptionIfRemoteBuildMissing()
{
$this->expectException(\Twig\Error\RuntimeError::class);
$this->expectExceptionMessage('Could not find the entrypoints file from URL "https://example.com/missing_build/entrypoints.json": the HTTP request failed with status code 404.');

$kernel = new WebpackEncoreIntegrationTestKernel(true);
$kernel->outputPath = 'remote_build';
$kernel->builds = ['remote_build' => 'https://example.com/missing_build'];
$kernel->boot();
$twig = $this->getTwigEnvironmentFromBootedKernel($kernel);
$twig->render('@integration_test/template_remote.twig');
}

public function testDisabledStrictModeIgnoresMissingRemoteBuild()
{
$kernel = new WebpackEncoreIntegrationTestKernel(true);
$kernel->outputPath = 'remote_build';
$kernel->strictMode = false;
$kernel->builds = ['remote_build' => 'https://example.com/missing_build'];
$kernel->boot();
$twig = $this->getTwigEnvironmentFromBootedKernel($kernel);
$html = $twig->render('@integration_test/template_remote.twig');
self::assertSame('', trim($html));
}

public function testAutowireableInterfaces()
{
$kernel = new WebpackEncoreIntegrationTestKernel(true);
Expand Down Expand Up @@ -228,6 +275,7 @@ class WebpackEncoreIntegrationTestKernel extends Kernel
public $outputPath = __DIR__.'/fixtures/build';
public $builds = [
'different_build' => __DIR__.'/fixtures/different_build',
'remote_build' => 'https://example.com/build',
];
public $scriptAttributes = [];

Expand Down Expand Up @@ -261,6 +309,9 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa
'enabled' => $this->enableAssets,
],
'test' => true,
'http_client' => [
'mock_response_factory' => WebpackEncoreHttpClientMockCallback::class,
],
];
if (self::VERSION_ID >= 50100) {
$frameworkConfig['router'] = [
Expand Down Expand Up @@ -310,6 +361,9 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa
// @legacy for 5.0 and earlier: did not have controller.service_arguments tag
$container->getDefinition('kernel')
->addTag('controller.service_arguments');

$container->register(WebpackEncoreHttpClientMockCallback::class)
->setPublic(true);
}

public function getCacheDir(): string
Expand Down Expand Up @@ -365,3 +419,46 @@ public function __construct(EntrypointLookupInterface $entrypointLookup, Entrypo
{
}
}

class WebpackEncoreHttpClientMockCallback
{
/** @var callable|null */
public $callback;

public function __invoke(string $method, string $url, array $options = []): ResponseInterface
{
$callback = $this->callback ?? static function (string $method, string $url) {
if ('GET' === $method && 'https://example.com/build/entrypoints.json' === $url) {
return new MockResponse(json_encode([
'entrypoints' => [
'app' => [
'js' => [
'https://cdn.example.com/app.js?v=abcde01',
],
'css' => [
'https://cdn.example.com/app.css?v=abcde02',
],
],
'backend' => [
'js' => [
'https://cdn.example.com/backend.js?v=abcde01',
],
'css' => [
'https://cdn.example.com/backend.css?v=abcde02',
],
],
],
], flags: \JSON_THROW_ON_ERROR), [
'http_code' => 200,
'response_headers' => ['Content-Type: application/json'],
]);
}

return new MockResponse('Not found.', [
'http_code' => 404,
]);
};

return ($callback)($method, $url, $options);
}
}
7 changes: 7 additions & 0 deletions tests/fixtures/manual_template_remote.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% for jsFile in encore_entry_js_files('backend', 'remote_build') %}
<script src="{{ asset(jsFile) }}"></script>
{% endfor %}

{% for cssFile in encore_entry_css_files('backend', 'remote_build') %}
<link rel="stylesheet" href="{{ asset(cssFile) }}" />
{% endfor %}
2 changes: 2 additions & 0 deletions tests/fixtures/template_remote.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ encore_entry_script_tags('app', null, 'remote_build') }}
{{ encore_entry_link_tags('app', null, 'remote_build') }}

0 comments on commit f5b9a1d

Please sign in to comment.