Trying to use xautoload on a PHP library that has PSR-0 compliant class names/namespaces, etc. Looking for a bit of guidance around a few things.

1) Since this is a non-Drupal PHP library, it does things like class Search extends \existing\namespace\Query which will not work with how xautoload expects \Drupal\$modulename to begin all class/interface names. Is this simply a matter of my module needing to register it's namespace and prefix explicitly and not relying on the PSR-0 D8-ish auto hookup?

2) If I wanted to put this library in sites/all/libraries instead of inside my $module/lib directory, is this something that is supported?

I appreciate for any guidance you can provide. If you can explain it in this issue and I get it to work, I will post a patch that adds some documentation around these use cases so you can avoid this question again.

Thanks.

UPDATE:
Libraries API integration is implemented, and documented here:
Support for sites/all/libraries (Libraries API)

An API change is being discussed:
#2297525: Libraries integration: Require libraries_load().
This will not affect you if you already call libraries_load(), as is recommended in the docs.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

donquixote’s picture

Hi,
first of all: I am currently traveling, so only giving some quick off-my-head answers.

there are two aspects to this.
1) The library/classes provided by xautoload.
2) How they are wired up / bootstrapped in Drupal.

1) the classes are designed to be wired up with any number of namespaces, handlers, whatsoever.
2) afaik, this problem has not even been addressed for D8 yet. The question is, when and how should Drupal modules register their classes in xautoload, that are not the usual D8-style PSR-0? I have a few ideas about this, but I am a bit worried this will end up in something custom-ish, and then D8 will come up with something different.

Basically there would be two options:
a) a hook to let modules register their additional namespaces. This requires that the module system is fully set up, and all modules are loaded - maybe too late for some modules.
b) modules specify extra namespaces and handlers in their .info files.
c) Your (custom) module actively calls xautoload_get_finder(), and registers the namespaces on the finder object. This could happen somewhere in hook_boot() or hook_init(), or whenever you need those classes.

I think the option (c) already works out of the box.
But, I am very open for any suggestions to make this as developer-friendly as possible.

donquixote’s picture

Another interesting option could be a scandir mode. But only if we can cache this somewhere.
Not a stupid scan-everything like the D7 registry, but something targeted to a specific namespace etc.

You see, plenty of possibilities, and your input is welcome :)

donquixote’s picture

In short, what you are looking for is something like this:

function mymodule_register_classes() {
  static $_firstRun = TRUE;
  if ($_firstRun) {
    $finder = xautoload_get_finder();
    $finder->registerNamespaceRoot('My\Funky\Library', 'sites/all/libraries/funkylib/lib');
    // or equivalent
    $finder->registerNamespaceDeep('My\Funky\Library', 'sites/all/libraries/funkylib/lib/My/Funky/Library');
    $_firstRun = FALSE;
  }
}

and call this from anywhere. Just avoid to register the same namespaces more than once.

This would be option (c) from #1.

febbraro’s picture

@donquixote thanks for the quick response. Let me play around a bit and try to get anything working based on your suggestions. Once I get something working I'll look at areas for improvement (or at least documentation). Many thanks!

febbraro’s picture

So I got it loading the library with my special namespace like so:

function mymodule_init() {
  $path = libraries_get_path('my-PHP-API');
  if (!empty($path)) {
    $finder = xautoload_get_finder();
    $finder->registerNamespaceRoot('MyNamespace', $path);
  }
}

I'm using the libraries module (2.x) to make it easier to detect the presence of the library. The one thing that has occurred to me so far is that with Libraries 2.x you specify the existence of your library via hook_libraries_info(). http://drupalcode.org/project/libraries.git/blob/refs/heads/7.x-2.x:/lib... So in that info array we could add an xautoload key along with the namespace we want to register and then xautoload could scan all libraries for that key in the "info" and process them automatically.

Does that sound like a direction you might be interested in heading? Should be pretty straight forward to throw a patch together around that if so..

donquixote’s picture

Ok, let's go for it!

Another idea would be a hook_xautoload(), where $finder would be exposed to implementing modules as an argument.

function mymodule_xautoload($finder) {
  $finder->registerNamespaceRoot('Exotic\\Namespace', drupal_get_path('module', 'mymodule') . '/exotic/xyz');
  $finder->registerNamespaceRoot('External\\Namespace', libraries_get_path('my-php-api') . '/lib');
}

Or alternatively we could just provide an $api object, instead of the $finder itself. This would allow e.g.
- to cache the result between requests.
- provide some sugary syntax shortcuts, to avoid drupal_get_path()
- overall untie the API from finder implementation details.

function mymodule_xautoload($api) {
  // automatically assuming module = mymodule
  $api->registerNamespaceRoot('Unregular\\Namespace', 'external/xyz');
  // Automatically does the libraries_get_path() for us.
  $api->registerNamespaceRoot('External\\Namespace', 'lib', 'library:my-php-library');
}

The issue I have with all this is that it will probably be different than what D8 will do. But i guess we have to live with that, if we want to make any progress.

febbraro’s picture

Status: Active » Needs review
FileSize
991 bytes

Wanted to get something in front of you to validate. This is going on my initial assumption in #5 this allows a user to specify a library as so.

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'xautoload' => array('namespace' => 'My\Namespace'),
  );
  return $libraries;
}
donquixote’s picture

'path' setting:
It would be great if one could specify a relative path there (relative to library path), instead of an absolute one.

hook_init():
I wonder if this can be too late in some cases.
We want it to run as soon as possible, but not before libraries is fully available. I have no idea when that could be :)

Multiple namespaces:
What if there is more than one namespaces / subfolders to register for the library?

I wonder, does this have a benefit vs hook_xautoload($api) as in #6 bottom ?

donquixote’s picture

I wonder, does this have a benefit vs hook_xautoload($api) as in #6 bottom ?

Answering myself: Yes it has.
The settings can be in hook_libraries_info() even if the library is not installed, and not knowing the final filesystem location.

I would really like to play with the $api thing, instead of some confusing info array structure.
Do you think we can use anonymous functions?

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'xautoload' => function($api) {
      $api->registerNamespaceRoot('My\\Namespace', 'lib');
      $api->registerNamespaceRoot('My\\Namespace', 'lib-fallback');
      $api->registerNamespaceRoot('My\\Namespace2', 'lib2');
      $api->registerNamespaceHandler(...);
      // scandir() for sub-namespaces
      $api->scanSubNamespaceDirs('My\\Namespace3', 'lib3');
    },
  );
  return $libraries;
}

This would allow for all kinds of multiplicity, shortcuts for scandir() stuff, etc. It could have the same syntax as for hook_xautoload($api) - although some methods on $api could be specific to libraries.

I somehow prefer this to large nested info arrays.
Why?
- documentation can happen on the methods of the $api object's class.
- validation / sanity check can be built into the $api methods. So we avoid saving a malformed info array.

(I am throwing ideas around, I hope you don't lose your patience reading it :) )

febbraro’s picture

Would specifying a callback in the libraries info be more to your liking? What I was trying to do was make is dirt simple for folks with straighforward libraries, but you are right, maybe we need to handle simple and complex scenarios. What about the simple thing I wrote the patch for and/or the ability to specify a callback like such.

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'xautoload' => array('namespace' => 'My\Namespace')
    // OR
    'xautoload callback' => 'mymodule_xautoload_callback',
  );
  return $libraries;
}

function mymodule_xautoload_callback($api) {
      $api->registerNamespaceRoot('My\\Namespace', 'lib');
      $api->registerNamespaceRoot('My\\Namespace', 'lib-fallback');
      $api->registerNamespaceRoot('My\\Namespace2', 'lib2');
      $api->registerNamespaceHandler(...);
      // scandir() for sub-namespaces
      $api->scanSubNamespaceDirs('My\\Namespace3', 'lib3');
}
febbraro’s picture

As for your path specification. In reading up on hook_libraries_info() a bit more it appears there is already a path key that is optional for use as a subpath within the library, so no path needed I dont think in the xautoload section. In fact if we dont need to support path, then maybe the xautoload value is simply the namespace and nothing else as such..

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'path' => 'some/subdir'
    'xautoload' => 'My\Namespace'
    // OR
    'xautoload callback' => 'mymodule_xautoload_callback',
  );
  return $libraries;
}

donquixote’s picture

The 'path' setting might not be identical with psr-0 root.

What about sth like this (very rough brainstorming)

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'path' => 'some/subdir',
    // Make this the default psr-0 root
    'xautoload path' => 'some/subdir/lib',
    'xautoload namespaces' => array(
      // Make this 
      'My\Namespace',
      // Uses registerNamespaceRoot() by default.
      array('My\Namespace', 'lib'),
      array('My\Namespace', 'lib-alternative'),
      // Uses registerPrefixRoot() by default.
      array('My_Prefix', 'lib-underscore', 'prefix'),
      // Uses registerPrefixDeep() by default.
      array('My_Prefix', 'lib-underscore/My/Prefix', 'prefixDeep'),
    ),
    'xautoload callback' => 'mymodule_xautoload_callback',
  );
  return $libraries;
}

The idea would be that we can start with something simple, and then gradually extend the API.

febbraro’s picture

With the namespaces how would we know which finder function to use? Is this what the 3rd array parameter is specifying?

If we are to have this much data in the hook, then we should put it all under one xautoload key I think. I'm wondering though, do you really think it is worth putting all of that "special handling" in the info hook. Seems like if you have things like that, just use the callback. So, now thinking we may just want to do this..

/**
 * Implements hook_libraries_info()
 */
function mymodule_libraries_info() {
  $libraries['my-php-lib'] = array(
    'name' => 'My PHP Library',
    'vendor url' => 'http://www.example.com',
    'download url' => 'http://github.com/example/my-php-api',
    'version' => '1.0',
    'path' => 'some/subdir',
    'xautoload' => array(
      // Make this the default psr-0 root
      'path' => 'some/subdir/lib',
      // Specify namespace for very simple cases
      'namespaces' => 'My\Namespace'
      // Anything beyond simple needs to get done in the callback
      'callback' => 'mymodule_xautoload_callback',
    ),
  );
  return $libraries;
}
donquixote’s picture

Deal :)
This is not as powerful, but as you say, people can still use the callback (or fill that with an anonymous function).
And it is quite explicit in everything.

Plan of action:
1) Implement as suggested in #13.
2) Add hook_xautoload($api), which will behave similar to the callback.
3) Some time in the future when D8 has figured out how modules and libraries can register namespaces, we attempt to provide the same syntax in D7. The xautoload-specific solution will still be supported.

febbraro’s picture

Ok, here is a patch for #13.

Note: It is missing #14.2, hook_xautoload as I'm not exactly sure when you wanted to call that. Did you want that executed in hook_init when I do the rest of the libraries dancing or as part of _xautoload_register?

febbraro’s picture

Added @see hook_libraries_info()

donquixote’s picture

The "when" is a delicate question.

If you look at the "handler" I did for regular Drupal modules' PSR-0 classes, it totally circumvents this question:
The handler is registered very early, but at this time it does not do anything with modules yet. Only when the first module class is going to be autoloaded, will it call module_exists().

(have not looked at it for a while)

febbraro’s picture

Want to proceed with this patch and put that hook in later?

donquixote’s picture

Yes, let's do it piecemeal. We could even leave out the callback thing for now.

And to mention it again, I am traveling :)
I want to find some time when I can really look into the code and understand what I'm doing, before I commit this.

febbraro’s picture

The callback piece is already in the patch. Review when you return from you travels. Hope you are enjoying yourself. :)

donquixote’s picture

Now the comment that explains the callback uses a mysterious new $api->scanSubNamespaceDirs()
But then the callback is only used as $callback($finder).
It clearly cannot be committed like this :)

Anyway, I have a new plan:
- Start with hook_xautoload($api). For the start, let's invoke that from xautoload_init().
- For the $api argument we make a wrapper class xautoload_InjectedAPI_hookXautoload. For now this is just a wrapper around $finder, with some additional convenience methods.
- libraries_xautoload($api) implements hook_xautoload(), and does all the magic.
- libraries_xautoload() passes the $api object into the callbacks registered with libraries info.

Future:
- Find some tricks to register the stuff from hook_xautoload() as early as possible. hook_init() could be too late for some classes. Lazy evaluation can be the key.
- Instead of being a simple wrapper around $finder, the $api can write the result to a cache, so we don't have to fire hook_xautoload() every request.

donquixote’s picture

Awesome!
I made a patch focusing on the callback version, which seems overall more universally useful and elegant to me.
If we still want the array version, we can easily add it.

/**
 * Implements hook_libraries_info()
 * This is just a test.
 */
function hook_libraries_info() {
  return array(
    'mymodule-test-lib' => array(
      'name' => 'My test library',
      'vendor url' => 'http://www.example.com',
      'download url' => 'http://github.com/example/my-php-api',
      'version' => '1.0',
      'xautoload' => function($api) {
        // Register a namespace with PSR-0 root in <library dir>/lib/
        // Note: $api already knows the library directory.
        // Note: We could omit the 'lib', as this is the default value.
        $api->namespaceRoot('XALib\TestNamespace', 'lib');
      },
    ),
  );
}
donquixote’s picture

And other modules can also implement hook_xautoload()

function mymodule_xautoload($api) {
  // Declare a foreign namespace in (module dir)/lib/ForeignNamespace/
  $api->namespaceRoot('ForeignNamespace');
  // Declare a foreign namespace in (module dir)/vendor/ForeignNamespace/
  $api->namespaceRoot('ForeignNamespace', 'vendor');
  // Declare a foreign namespace in /home/username/lib/ForeignNamespace/,
  // setting the $relative argument to FALSE.
  $api->namespaceRoot('ForeignNamespace', '/home/username/lib', FALSE);
}
donquixote’s picture

Btw, originally I just wanted $api->namespace(), but php does not like that :)
(it's a reserved name, obviously)

donquixote’s picture

I just found that closures cannot be serialized. This could be a problem, if one day libraries decides that it wants to cache the return value of hook_libraries_info().

donquixote’s picture

#25:
Ok, and the problem is officially not a problem since today :)

If libraries ever decides to cache the return value, we simply do a hook_libraries_info_alter() to transform our stuff to something serializable:

function xautoload_libraries_info_alter() {
  foreach ($libraries as $libname => &$libinfo) {
    if (isset($libinfo['xautoload']) && is_callable($libinfo['xautoload'])) {
      $api = new xautoload_InjectedAPI_hookXautoload_LibInfoTransform();
      $api->setLibrary($libname);
      call_user_func($libinfo['xautoload'], $api);
      $libinfo['xautoload'] = $api->toInfoArray();
  }
}

InjectedAPI rocks!

@febbraro,
are you happy with the proposal as in #22 ?

donquixote’s picture

Title: Usage on existing PSR-0 compliant library, and how about sites/all/libraries? » Registration of additional PSR-0 folders in modules and sites/all/libraries
Category: support » feature
Status: Needs review » Fixed

Committed and pushed.

donquixote’s picture

Status: Fixed » Needs work

We need some documentation!
xautoload.api.php is non-existent.

donquixote’s picture

Status: Needs work » Fixed
Pol’s picture

I'm also having problems including a custom library with dependencies.

I've explained the problem here: http://drupal.stackexchange.com/questions/49692/include-a-third-party-li...

Status: Fixed » Closed (fixed)

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

donquixote’s picture

Issue summary: View changes

This is now documented here:
Support for sites/all/libraries (Libraries API)

An API change is being discussed:
#2297525: Libraries integration: Require libraries_load().
This will not affect you if you already call libraries_load(), as is recommended in the docs.