Skip to content

[Asset] Adding a new version strategy that reads from a manifest JSON file #22046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from

Conversation

weaverryan
Copy link
Member

@weaverryan weaverryan commented Mar 17, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR symfony/symfony-docs#7659

Hi guys!

Often, when using a frontend task manager or bundler (e.g. webpack of gulp), the final assets are dumped with a version or content hash in the filename itself (e.g. main.123abc.css). To know what the correct, current hashed filename is, you'll dump a manifest.json file - e.g.

{
  "main.js": "main.123abc.js",
  "css/styles.css": "css/styles.555def.css"
}

Examples: gulp-rev and webpack-manifest-plugin.

This PR adds a new version strategy that will look up the asset path (e.g. main.css) in that file and return the final, versioned path. Some people may dump manifest files in other formats, but I think this catches the most common use-case (and you can always still create your own version strategy). I've written this to be "forgiving" - if a path doesn't exist in the manifest, the path is simply returned, unaltered.

Another implementation could have been to add a new Twig filter (e.g. {{ asset('main.css|manifest_path) }}) - but I thought I'd try first using the existing versioning system.

Usage

# app/config/config.yml
framework:
    # ...
    assets:
       # added validation prevents you from setting json_manifest_path AND version, for example
        json_manifest_path: '%kernel.root_dir%/../web/manifest.json'
{# someTemplate.html.twig #}

{# use asset() just like normal #}
<script src="{{ asset('js/main.js') }}"></script>

TODO

  • fabbot hates my invalid json syntax file... even though I tried to be clever and not give it a .json suffix :)

Copy link
Member

@javiereguiluz javiereguiluz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A big 👍 from me!

This is so common in modern setups that Symfony should really provide this asset versioning.

I also agree with being forgiving and returning the same path when it doesn't exist in the manifest file.

@@ -589,6 +597,12 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode)
})
->thenInvalid('You cannot use both "version_strategy" and "version" at the same time under "assets" packages.')
->end()
->validate()
->ifTrue(function ($v) {
return isset($v['version_strategy']) && $v['version_strategy'] == 'json_manifest' && !isset($v['manifest_path']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== 'json_manifest' -> === 'json_manifest'

@@ -558,6 +559,12 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode)
})
->thenInvalid('You cannot use both "version_strategy" and "version" at the same time under "assets".')
->end()
->validate()
->ifTrue(function ($v) {
return isset($v['version_strategy']) && $v['version_strategy'] == 'json_manifest' && !isset($v['manifest_path']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== 'json_manifest' -> === 'json_manifest'

{
"main.js": main.123abc.js",
"css/styles.css": "css/styles.555def.css"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a new line at the end of this file.

@@ -0,0 +1,4 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clever to not use .json extension to avoid fabbot issues ... but that could complicate the maintenance of this. Imagine looking at this conde in 1 year and wondering: why this file doesn't have an extension? Is this an error? Should I add the extension now? etc.

So, let's add the .json extension and forget about fabbot issues.

@thewilkybarkid
Copy link
Contributor

thewilkybarkid commented Mar 20, 2017

Looks to replace https://github.com/irozgar/gulp-rev-versions-bundle.

A useful feature would be to compile the manifest into a static array, to avoid reading it at runtime (in production).

@stof
Copy link
Member

stof commented Mar 21, 2017

This changes the meaning of the version_strategy configuration, as this is not a service id anymore in some cases.
IMO, the usage of the json manifest strategy should be detected based only on manifest_path being configured and no custom strategy being provided, similar to the way the static version strategy is configured

{
$manifestPath = $this->getManifestPath($path);

return $manifestPath ? $manifestPath : $path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return $manifestPath ?: $path;


private $manifestData;

public function __construct($manifestPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add the phpdoc describing the argument as a string, as we don't have a typehint to tell it

}

$this->manifestData = json_decode(file_get_contents($this->manifestPath), true);
if (0 < $errorCode = json_last_error()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$errorCode is unused

{
$def = new ChildDefinition('assets.json_manifest_version_strategy');
$def
->replaceArgument(0, $manifestPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we should also make this service private, to allow it to be inlined

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I copied this code from the above method, which is not private. So, might there be a reason for it to be public?

private function createPackageDefinition($basePath, array $baseUrls, Reference $version)
{
if ($basePath && $baseUrls) {
throw new \LogicException('An asset package cannot have base URLs and base paths.');
}
$package = new ChildDefinition($baseUrls ? 'assets.url_package' : 'assets.path_package');
$package
->setPublic(false)
->replaceArgument(0, $baseUrls ?: $basePath)
->replaceArgument(1, $version)
;
return $package;

I'm going to try making it private.

@GromNaN
Copy link
Member

GromNaN commented Mar 23, 2017

Awesome feature that I've seen implemented in many projects, this JSON format is so simple that it is always the one that is used.
Is it possible to have many manifests ? I remember having JS, CSS and images assets in different manifests files. But also mobile and desktop assets in different manifests.

Is the solution to define an asset package for each manifest file ?

@javiereguiluz
Copy link
Member

@GromNaN is having multiple manifest still common? I only know the Webpack solution and they create just 1 manifest.json for all assets(images, CSS, JS).

@javiereguiluz javiereguiluz added this to the 3.3 milestone Mar 23, 2017
@weaverryan weaverryan force-pushed the asset_manifest_path branch from 7a652ae to 99251c3 Compare March 23, 2017 10:53
@weaverryan
Copy link
Member Author

PR Updated!

  1. I like Stof's suggestion about keeping version_strategy always as an id. I updated the config usage in the PR description

  2. @thewilkybarkid I don't know if compiling a static array from the JSON is a good idea or not. It might be unexpected that simply updating the file wouldn't cause Symfony to pick up the changes. I'm not sure if the decoding overhead is significant enough to care - decoding a 30 line manifest takes .00011 seconds (.11 milliseconds), though it is a filesystem hit!

  3. @GromNaN That's interesting, I want to make sure we've at least thought through this scenario (though ultimately, the strategy doesn't need to work for everyone. In general, can you think of a solution other than multiple packages? Even if we made this feature much bigger and supported multiple manifest files, in Twig, you still would need to specify which one you need, and the only config you can pass to asset() is the $packageName. Or, the strategy would need to somehow be smart enough to know which manifest file to use for each package, which now sounds like you would/should have a custom version strategy.

This is ready to be reviewed again! Thanks!

@@ -582,6 +595,7 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode)
->end()
->end()
->scalarNode('version_format')->defaultNull()->end()
->scalarNode('json_manifest_path')->defaultNull()->end()
Copy link
Member

@javiereguiluz javiereguiluz Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just asking: would manifest_path be a more future-proof name than json_manifest_path ? Or is this too much future thinking? Can we expect to support other formats different than JSON?

Same for JsonManifestVersionStrategy -> ManifestVersionStrategy ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good thought. I don't know! It's a fairly easy thing to deprecate and change in the future anyways, and I can't see any other format in the near future. So let's go with what we like best right now. I changed to json_manifest_path because I thought it might be more clear what this manifest thing should be.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm as json*

@GromNaN
Copy link
Member

GromNaN commented Mar 23, 2017

@javiereguiluz @weaverryan My actual use case is the following:
Having a website on www.example.com with an mirror mobile site m.example.com
Routing and controllers are the same for both domains, but depending on the domain the view listener render a different template (thanks to Twig namespaces).
Both websites are very different in terms of design, that why the frontend developers chose to have 2 separate builds, generating 2 separate JSON manifests. The "mobile" templates use "mobile" assets while the "desktop" templates use "desktop" assets.


Regarding the usage of a PHP file to cache the content of the JSON into a PHP array, from my experience on high traffic websites there is no performance issue at reading the same JSON file on each request: the OS caches the file content in memory and native JSON parsing is very fast.

@weaverryan
Copy link
Member Author

@GromNaN Hmm, in that case, it would be most convenient for the listener to be able to change the manifest file. If you created a default package and a mobile package, then in the listener, if you're on mobile, you could make the mobile package the default:

// in the listener... if mobile
// using the container for clarity
$packages = $this->container->get('assets.packages');
$packages->setDefaultPackage($packages->getPackage('mobile'));

And then your templates wouldn't need to specify a package everywhere. Of course, since they're using different templates, they could just specify the mobile package when using asset() instead of the above code.

I think this is actually a pretty decent solution. In other words, I think this PR solves the 99%, and makes the 1% reasonably easy.

Thanks for the reply :)

@fabpot
Copy link
Member

fabpot commented Mar 23, 2017

@weaverryan For the invalid JSON file, just add it to .php_cs.dist

@@ -4,6 +4,8 @@ CHANGELOG
3.3.0
-----

* A new version strategy option called json_manifest_path was added
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a new ...

@@ -1,6 +1,11 @@
CHANGELOG
=========

3.3.0
-----
* JsonManifestVersionStrategy was added as a way to read final,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added Json...

* "css/styles.css": "css/styles.555abc.css"
* }
*
* You could then as for the version of "main.js" or "css/styles.css".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: ask?

class JsonManifestVersionStrategy implements VersionStrategyInterface
{
private $manifestPath;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty line can be removed

private $manifestData;

/**
* @param string $manifestPath Absolute path to the manifest file.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no dot at the end

*
* @param string $path
*
* @return string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both @param and @return can be removed

@GromNaN
Copy link
Member

GromNaN commented Mar 23, 2017

The manifest could be a remote file accessible over HTTP ; that appends when assets and the app are deployed on distinct servers asynchronously. In that case, it is necessary to use an HTTP client (other that file_get_contents) and a cache layer.
This feature can be added once this PR is merged.

@fabpot
Copy link
Member

fabpot commented Mar 23, 2017

@GromNaN I would say that we don't want to support that use case.

@fabpot
Copy link
Member

fabpot commented Mar 23, 2017

👍

@weaverryan
Copy link
Member Author

@GromNaN I thought about that use-case, it's also relevant with webpack-dev-server, but ultimately, I didn't want it, at least for now: the user should make sure there's a physical JSON file - it could be part of their deploy process (or, they can of course add their own strategy!)

Tweaks made! But fabbot and I are still fighting - is the JSON check done through PhpCsFixer? Or did I just not get the config right in .php_cs.dist?

@weaverryan
Copy link
Member Author

@fabpot if my silly invalid JSON file is going to be a problem with fabbot, let me know - I could re-work things - i.e. save a tmp invalid JSON file in the test and use that (so no committed invalid JSON file).

@fabpot
Copy link
Member

fabpot commented Mar 23, 2017

@weaverryan Don't worry about that. I will see why it's not taken into account by fabbot. Keep the code as is.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 with optional comments

@@ -4,6 +4,8 @@ CHANGELOG
3.3.0
-----

* Added a new new version strategy option called json_manifest_path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quotes around "json_manifest_path"?

@@ -4,6 +4,8 @@ CHANGELOG
3.3.0
-----

* Added a new new version strategy option called json_manifest_path
that allows you to use the JsonManifestVersionStrategy.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backticks here?

@@ -582,6 +595,7 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode)
->end()
->end()
->scalarNode('version_format')->defaultNull()->end()
->scalarNode('json_manifest_path')->defaultNull()->end()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm as json*

if (null !== $jsonManifestPath) {
$def = new ChildDefinition('assets.json_manifest_version_strategy');
$def->replaceArgument(0, $jsonManifestPath);
$def->setPublic(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to xml?

{
$manifestPath = $this->getManifestPath($path);

return $manifestPath ?: $path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be a oneliner :)

@weaverryan
Copy link
Member Author

Minor comments addressed!

@fabpot
Copy link
Member

fabpot commented Mar 25, 2017

Thank you @weaverryan.

@fabpot fabpot closed this in 8901c7e Mar 25, 2017
@weaverryan weaverryan deleted the asset_manifest_path branch March 25, 2017 17:22
@fabpot fabpot mentioned this pull request May 1, 2017
xabbuh added a commit to symfony/symfony-docs that referenced this pull request Jun 22, 2017
…tion (robfrawley)

This PR was submitted for the master branch but it was merged into the 3.3 branch instead (closes #8004).

Discussion
----------

[WIP] Document "framework.assets.json_manifest_path" option

This pull request adds initial documentation for the newly introduced asset JSON manifest file path option: `framework.assets.json_manifest_path`. Resolves #7659.

For reference on this new configuration option, see symfony/symfony#22046 and [symfony.com/blog/new-in-symfony-3-3-manifest-based-asset-versioning](https://symfony.com/blog/new-in-symfony-3-3-manifest-based-asset-versioning).

Commits
-------

13a3070 document new framework.assets.json_manifest_path option
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants