This came up in #2350877: Deprecate/rename drupal_add_feed(), drupal_add_html_head(), drupal_add_html_head_link(), drupal_add_http_header(), and allow to be set declaratively in #attached, resp. #2358727: Figure out what if anything libraries API needs for #attached support

Problem/Motivation

There is currently no way to dynamically register libraries. Libraries API will certainly need to do this in contrib, and there are other use-cases as well.

You could always use hook_library_info_alter()to also register new ones, but it results in a alter/weight hell.

Proposed resolution

Introduce a

hook_library_info_build()

which will be fired before executing hook_library_info_alter()

Remaining tasks

User interface changes

API changes

https://www.drupal.org/node/2402315

Contributor tasks needed
Task Novice task? Contributor instructions Complete?
Update the issue summary Add the mentioned API additition and add a beta evaluation

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue priority Critical because this blocks a stable release of Libraries API
Unfrozen changes Not unfrozen.
Prioritized changes The main goal of this issue is to move Drupal 8 closer to a releasable state by unblocking a stable release of Libraries API.
Disruption Disruption is minimum because this doesn't replace existing functions.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tstoeckler’s picture

So I think it will have to be one of the three:

1. Throw an event

2. Provide a separate hook_libraries_build() or similar

3. Provide support for a libraries_callback in the *.libraries.yml

All of these are existing patterns in core, however, when it comes to things defined in YAML the precedent leans towards number 3. I personally think that's a terrible anti-pattern and I involuntarily growl at my laptop everytime I see it, but if that's what's needed to get this in, so be it.

tstoeckler’s picture

Priority: Normal » Critical

This blocks a stable release of Libraries API, so marking critical per #2358727-2: Figure out what if anything libraries API needs for #attached support.

Berdir’s picture

We already have two alter hooks. One to alter the static library definitions (https://api.drupal.org/api/drupal/core%21modules%21system%21system.api.p...) and one to alter a specific library at runtime, when it is used on a page: https://api.drupal.org/api/drupal/core%21modules%21system%21system.api.p....

So i think the only difference for a build hook/callback would be that it can define something completely new.

As mentioned in the other issue, in jw_player, I currently define a dummy entry in libraries.yml that I then alter, so that part would go away. http://cgit.drupalcode.org/jw_player/tree/?h=8.x-1.x and http://cgit.drupalcode.org/jw_player/tree/jw_player.module?h=8.x-1.x#n92 down.

xjm’s picture

xjm’s picture

Is #3 a complete workaround for the problem? If so I would demote this to major.

tstoeckler’s picture

No it is not. We have bitten ourselves in the foot many times before when we didn't abide to our usual build-then-alter workflow. The problem is that no one is able to alter the library information declared in such a way reliably. We definitely need a new hook or mechanism of some sort.

tstoeckler’s picture

Status: Active » Needs review
FileSize
8.05 KB

Here's a first patch.

dawehner’s picture

Just adding a couple of tags ... this implementation is pretty good.

  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -221,13 +219,23 @@ public function buildByExtension($extension) {
    +      catch (InvalidDataTypeException $e) {
    +        // Rethrow a more helpful exception to provide context.
    +        throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
    +      }
    

    <3

  2. +++ b/core/modules/system/theme.api.php
    @@ -644,6 +644,70 @@ function hook_js_alter(&$javascript) {
    +  if (REQUEST_TIME < date_sunrise(REQUEST_TIME) || REQUEST_TIME > date_sunset(REQUEST_TIME)) {
    

    This is quite a day to day usecase ...

dawehner’s picture

... beside the tags it seems to be done now.

dawehner’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Updated the IS, added a CS

One thing which is kinda confusing for me is that the alter hook is executed for every extension, but this is out of scope for this issue, for sure,
its just odd.

Wim Leers’s picture

Code looks good, test coverage looks sane.

+++ b/core/modules/system/theme.api.php
@@ -644,6 +644,70 @@ function hook_js_alter(&$javascript) {
+  if (REQUEST_TIME < date_sunrise(REQUEST_TIME) || REQUEST_TIME > date_sunset(REQUEST_TIME)) {
+    $libraries['mymodule.vampire'] = [

ROFL :D

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
@@ -66,10 +66,8 @@ public function buildByExtension($extension) {
     $library_file = $path . '/' . $extension . '.libraries.yml';

Shouldn't this move into parseLibraryInfo() and remove $library_file as a parameter?

Additionally, looking through where the $library_file parameter is used later:

// If this is a 3rd party library, the license info is required.
if (isset($library['remote']) && !isset($library['license'])) {
throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for '%s' in %s: it has a remote, but no license.", $id, $library_file));
}

This is not touched in the patch, but the incomplete license may not be in the file, it could be in the hook instead.

tstoeckler’s picture

Ahh thanks @catch that makes sense. I had the same thought originally, but then reverted that because $library_file is used later on. I did not check whether that even makes sense with the patch. You are right that it doesn't, at least not necessarily.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
2.69 KB
9.69 KB

Here we go.

I went the way of removing the file info from the exception messages. We don't have a canonical way of finding the library source (i.e. is it from a file and from which or is it from an info hook and from which), so I don't see how to do that easily.

Status: Needs review » Needs work

The last submitted patch, 14: 2358981-14-library-info-dynamic.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
866 bytes
10.54 KB

Status: Needs review » Needs work

The last submitted patch, 16: 2358981-16-library-info-dynamic.patch, failed testing.

dawehner’s picture

Once we would have figured out #2378737: Consider not executing hook_library_(info)_alter() *per* library, but once for all properly, this issue would probably be just easier, given that there is just once place where the alter is called.

Wim Leers’s picture

Title: Provide a mechanism for dynamic library declarations » [PP-1] Provide a mechanism for dynamic library declarations

Per #18.

plach’s picture

Status: Needs work » Postponed

Per #19 :)

dawehner’s picture

Status: Postponed » Needs review
Issue tags: +Ghent DA sprint

So the other issue didn't went anymore, @tstoeckler do you want to (probably) reroll your patch and get it in?

Wim Leers’s picture

Title: [PP-1] Provide a mechanism for dynamic library declarations » Provide a mechanism for dynamic library declarations

Agreed that this is unblocked.

Status: Needs review » Needs work

The last submitted patch, 16: 2358981-16-library-info-dynamic.patch, failed testing.

Devin Carlson’s picture

Status: Needs work » Needs review
FileSize
3.85 KB
11.35 KB

A reroll of #16.

Wim Leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -77,15 +77,11 @@ public function buildByExtension($extension) {
    -        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
    +        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s'", $id));
    
    @@ -102,7 +98,7 @@ public function buildByExtension($extension) {
    -        throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for '%s' in %s: it has a remote, but no license.", $id, $library_file));
    +        throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for '%s': it has a remote, but no license.", $id));
    

    Instead of logging the file name, which is indeed not possible for dynamically discovered libraries, we should log the extension. Because there may be multiple libraries with the same name, but in different extensions.

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -218,14 +214,26 @@ public function buildByExtension($extension) {
    +    // Allow modules to add dynamic library definitions.
    +    $hook = 'library_info_build';
    +    if ($this->moduleHandler->implementsHook($extension, $hook)) {
    +      $libraries = array_merge($libraries, $this->moduleHandler->invoke($extension, $hook));
         }
    

    Why add a new hook, why not just explicitly make it possible for hook_library_info_alter() to *add* libraries?

    If some other extension wants to alter it, it can still use hook_module_implements_alter() to make its alter hook run later?

  3. +++ b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
    @@ -375,6 +375,28 @@ function testLibraryAlter() {
    +   * Dynamically defines a JavaScript library and alters it.
    

    s/a JavaScript library/an asset library/

tstoeckler’s picture

Why add a new hook, why not just explicitly make it possible for hook_library_info_alter() to *add* libraries?

If some other extension wants to alter it, it can still use hook_module_implements_alter() to make its alter hook run later?

No way. What you describe should be avoided at almost all costs. Experience in core and contrib has shown that this simply does not work in any reliable way. Therefore, we consistently build-then-alter all over core and this should follow suit.

dawehner’s picture

Why add a new hook, why not just explicitly make it possible for hook_library_info_alter() to *add* libraries?

If some other extension wants to alter it, it can still use hook_module_implements_alter() to make its alter hook run later?

This strategy prooved to be not successfull in Drupal 7. We do have the same strategy in many other places (route rebuilding, entity type definitions etc.).

Wim Leers’s picture

Calm down guys, I was merely playing the devil's advocate, to make sure we have a documented (in this issue), solid reason to do an API addition. Well answered :)

webchick’s picture

Issue tags: +Triaged D8 critical

Core committers discussed today and agreed we need this.

dawehner’s picture

+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
@@ -218,14 +214,26 @@ public function buildByExtension($extension) {
+      $libraries = array_merge($libraries, $this->moduleHandler->invoke($extension, $hook));

Should we maybe use NestedArray::merge() here instead?

Also we need to address #26 1,3 on top of it.

mpdonadio’s picture

Status: Needs work » Needs review
FileSize
11.67 KB
2.58 KB

#26:1,3; #31 addressed.

Status: Needs review » Needs work

The last submitted patch, 32: provide_a_mechanism_for-2358981-32.patch, failed testing.

The last submitted patch, 32: provide_a_mechanism_for-2358981-32.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
Issue tags: +CriticalADay
FileSize
1.37 KB
11.71 KB

Fixes phpunit fails

dawehner’s picture

+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
@@ -77,15 +78,11 @@ public function buildByExtension($extension) {
-        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
+        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for extension '%s'", $extension));
       }

@@ -102,7 +99,7 @@ public function buildByExtension($extension) {
-        throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for '%s' in %s: it has a remote, but no license.", $id, $library_file));
+        throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for extension '%s': it has a remote, but no license.", $extension));

Let's give as much context as possible and add both the extension and the library ID

mpdonadio’s picture

Addresses #37

larowlan’s picture

FileSize
3.32 KB
12.13 KB

agree

larowlan’s picture

cross-post

dawehner’s picture

Status: Needs review » Needs work

Can we please write the beta evaluation and add the api changes to the issue summary?

dawehner’s picture

Issue summary: View changes

That itself is a novice task

dawehner’s picture

Issue tags: +Novice

.

dawehner’s picture

.

tadityar’s picture

Issue summary: View changes

Added beta evaluation and draft change records here https://www.drupal.org/node/2402315 .

tadityar’s picture

Status: Needs work » Needs review
dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Novice

Thank you a lot!

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/system/theme.api.php
@@ -722,6 +722,70 @@ function hook_js_alter(&$javascript) {
+  if (REQUEST_TIME < date_sunrise(REQUEST_TIME) || REQUEST_TIME > date_sunset(REQUEST_TIME)) {

I know it's just a docs example, but since the definitions are cached, this wouldn't work without clearing the cache on a schedule or altering the cache key of the library discovery to take into account sunset/sunrise. So could we do a less dynamic example like module_exists() where we know the definition will get re-discovered at the appropriate time? Could also use a note in the hook documentation that the results are cached.

The config()->get() is similar as well, nothing rebuilds library definitions on config save.

tadityar’s picture

Status: Needs work » Needs review
FileSize
12.05 KB
846 bytes

Should it be something like this?

Status: Needs review » Needs work

The last submitted patch, 49: library-dynamic-2358981-49.patch, failed testing.

tadityar’s picture

Status: Needs work » Needs review
FileSize
12.07 KB
781 bytes

Trying to pass..

dawehner’s picture

+++ b/core/modules/system/theme.api.php
@@ -743,7 +743,7 @@
-  if (module_exists('somebodyelsemodule')) {
+  if (Drupal::moduleHandler()->moduleExists('minizombies')) {
     $libraries['mymodule.zombie'] += [

@@ -769,7 +769,7 @@
   // the library (of course) not be loaded but no notices or errors will be
   // triggered.
-  if (module_exists('mymodule')) {
+  if (module_exists('vampireize')) {
     $libraries['mymodule.vampire'] = [
       'js' => [
         'js/vampire.js' => [],

Let's use the module handler in both places

tadityar’s picture

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you, this looks fine for me.

We do have a change record, proper issue summary as well as a beta evaluation.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for changing the examples (and sorry for spoiling the nice joke :().

Committed/pushed to 8.0.x, thanks!

  • catch committed 73e5bfa on 8.0.x
    Issue #2358981 by tadityar, tstoeckler, larowlan, mpdonadio, Devin...
Wim Leers’s picture

Great work! :)

Wim Leers’s picture

The only problem: we have TWO change record drafts! :D This must be an unprecedented problem we've had in the D8 cycle :D

There's both https://www.drupal.org/node/2402315 and https://www.drupal.org/node/2374649. Which to publish? I think we should pick the best parts of both.

tadityar’s picture

vampires need module now :))

@WimLeers
Wow, sorry I didn't notice one was created prior to mine..

Wim Leers’s picture

Don't worry, this is a *great* problem to have :)

tadityar’s picture

I've updated this change record https://www.drupal.org/node/2374649 anything to revise?

nod_’s picture

CR need to say that this hook is invoked before library information is cached and not dynamically invoked on every page request. See #48.

dawehner’s picture

@nod_
Right, this is the idea. I agree the word dynamic can be confusing, but its dynamic relative to use a static yml file.

Wim Leers’s picture

#62/#63: added that, and published the CR: https://www.drupal.org/node/2374649

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

jhedstrom’s picture

It was surfaced in #2418907: _drupal_add_js()/css() removed from core that this implementation doesn't allow for libraries to reside outside of the module implementing hook_library_info_build(), which is the main point of the Libraries API module (as it manages libraries in sites/all/libraries or PROFILE/libraries).

Dom.’s picture

I know this issue is over, but still wondering: why changing the pattern every when and since ? Sometimes it's an event, sometimes it's a callback method (dynamic permissions for instance), sometimes it's a hook (like here). Wasn't it a point of D8 to be OOP, thus avoid hooks ?
Why not agree on a unique strategy and stick to it ? That way, when a dev wants to do something, he knows what to search for. Let say it's a callback like in permission, he knows he simply have to find the callback name for dynamic libraries.

Also, more OOP, isn't it possible to define abstract class for things, let says AbstractLibraryDefinition and instead of invoking a hook, invoke all modules class that would implements AbstractLibraryHandler and implements it's virtual method "buildDynamicPermission()" for instance, then its method "alterPermissions($permissions)" and so on ?

Something like (naively):

foreach (get_declared_classes() as $className) {
    if (in_array('AbstractLibraryDefinition', class_implements($className))) {
        // Get dynamic libraries. 
        $libraries = NestedArray::mergeDeep($className->buildDynamicPermission());
    }
}

This is highly likely to be a stupid question, but if I don't ask, I will never learn !
Thanks all !