Skip to content
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

Builtin dependencies #318

Closed
thekid opened this issue Sep 3, 2016 · 6 comments
Closed

Builtin dependencies #318

thekid opened this issue Sep 3, 2016 · 6 comments

Comments

@thekid
Copy link
Member

thekid commented Sep 3, 2016

Scope of Change

Usecase: Standalone scripts.

Rationale

$ xp query.script.php 'mysql://localhost' 'select 1'
*** Error for argument #1: Exception lang.Error (Class 'rdbms\DriverManager' not found)

If not inside a directory where we've configured and run Composer, we will need to type xp -m /path/to/rdbms/module query.script.php ... (and figure out transitive dependencies) ourselves.

Functionality

The class or script needs to be able to declare what modules it requires. These could be loaded via the composer global project (with all its downsides)

<?php namespace github_users;

use text\json\StreamInput from 'xp-forge/json';
use util\data\Sequence from 'xp-forge/sequence';
use util\cmd\Console from 'xp-framework/core';
use util\log\{LogCategory, ConsoleAppender} from 'xp-framework/logging';
use webservices\rest\{Endpoint, Links} from 'xp-framework/rest';

class GitHubApi extends Endpoint {

  public function __construct(string $token) {
    parent::__construct('https://api.github.com/');
    $this->with(['Authorization' => 'token '.$token, 'User-Agent' => nameof($this)]);
  }

  public function paged(string $resource): iterable {
    do {
      $response= $this->resource($resource)->accepting('application/json')->get();
      yield from (new StreamInput($response->stream()))->elements();

      $links= new Links($response->header('Link'));
      $resource= $links->uri(['rel' => 'next']);
    } while ($resource);
  }
}

$api= new GitHubApi(getenv('GITHUB_TOKEN'));
if (in_array('-v', $argv)) {
  $api->setTrace((new LogCategory())->withAppender(new ConsoleAppender()));
}

Sequence::of($api->paged('orgs/1and1/members?per_page=50'))
  ->each(function($user) { Console::writeLine($user['type'], ' ', $user['login']); })
;

Security considerations

Speed impact

Dependencies

Related documents

TypeScript modules / import and export:

Groovy

Composer global downsides / discussion:

@thekid
Copy link
Member Author

thekid commented Sep 3, 2016

Solution inside PHP code

  • ➖ Duplicates existing knowledge about Composer paths in XP Runners
  • ➖ XP Release dependant
  • ➕ Works in all places
  • ➕ Can easily be extended to work with exports / wildcards etcetera.

Declaration

A from "statement" instead of use, in the entrypoint class com/example/Query.class.php:

<?php namespace com\example;

from('xp-framework/rdbms', ['rdbms\{DriverManager, ResultSet}']);
from('xp-framework/logging', ['util\log\*']);
from('xp-framework/command', ['util\cmd\Command']);

/**
 * Performs an SQL query
 */
class Query extends Command {
  // ...
}

...or at the top of the script ageindays.script.php:

<?php namespace ageindays;

from('xp-framework/core', ['util\{Date, DateUtil}', 'util\cmd\Console']);

$span= DateUtil::timespanBetween(new Date($argv[1]), Date::now());
Console::writeLine('Hey, you are ', $span->getDays(), ' days old');

All other places still import types using use.

Implementation

(Uses Composer global modules, Windows only, not much error handling, goes inside lang.base.php)

function from($module, $imports, $namespace= null) {
  static $modules= ['php' => true, 'xp-framework/core' => true];

  if (null === $namespace) {
    $file= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'];
    $ns= \lang\ClassLoader::findUri($file)->path;
    $namespace= rtrim(dirname(substr($file, strlen($ns))), '.');
  }

  if (!isset($modules[$module])) {
    $base= getenv('APPDATA').'\\Composer\\vendor\\'.strtr($module, ['/' => '\\']).'\\';
    foreach (glob($base.'\\*.pth') as $file) {
      foreach (file($file) as $line) {
        $path= trim($line);
        if ('' === $path || '#' === $path{0}) {
          continue;
        } else if ('!' === $path{0}) {
          \lang\ClassLoader::registerPath($base.strtr(substr($path, 1), ['/' => '\\']), true);
        } else {
          \lang\ClassLoader::registerPath($base.strtr($path, ['/' => '\\']));
        }
      }
    }

    $modules[$module]= true;
    $composer= file_get_contents($base.'composer.json');
    foreach (json_decode($composer, true)['require'] as $depend => $_) {
      from($depend, [], $namespace);
    }
  }

  foreach ($imports as $import) {
    if ($p= strpos($import, '{')) {
      $base= substr($import, 0, $p);
      foreach (explode(', ', substr($import, $p + 1, -1)) as $type) {
        class_alias($base.$type, $namespace.'\\'.$type);
      }
    } else {
      class_alias($import, $namespace.substr($import, strrpos($import, '\\')));
    }
  }
}

Also requires a small patch to xp\runtime\Code:

diff --git a/src/main/php/xp/runtime/Code.class.php b/src/main/php/xp/runtime/Code.class.php
index 8507651..ee44154 100755
--- a/src/main/php/xp/runtime/Code.class.php
+++ b/src/main/php/xp/runtime/Code.class.php
@@ -8,6 +8,7 @@
  */
 class Code {
   private $fragment, $imports;
+  private $namespace= null;

   /**
    * Creates a new code instance
@@ -27,6 +28,13 @@ class Code {
     }

     $this->fragment= trim($input, "\r\n\t ;").';';
+
+    if (0 === strncmp($this->fragment, 'namespace', 9)) {
+      $length= strcspn($this->fragment, ';', 10);
+      $this->namespace= substr($this->fragment, 10, $length);
+      $this->fragment= substr($this->fragment, 10 + $length);
+    }
+
     $this->imports= [];
     while (0 === strncmp($this->fragment, 'use ', 4)) {
       $delim= strpos($this->fragment, ';');
@@ -53,7 +61,15 @@ class Code {

   /** @return string */
   public function head() {
-    return empty($this->imports) ? '' : 'use '.implode(', ', $this->imports).';';
+    if ($this->namespace) {
+      $head= 'namespace '.$this->namespace.';';
+      $head.= 'function from($module, $imports) { \from($module, $imports, __NAMESPACE__); }';
+    } else {
+      $head= '';
+    }
+
+    $head.= empty($this->imports) ? '' : 'use '.implode(', ', $this->imports).';';
+    return $head;
   }

@thekid
Copy link
Member Author

thekid commented Sep 4, 2016

Solution inside XP Runners

  • ➕ Can reuse existing Composer module logic
  • ➕ Will work with any XP Release
  • Needs to determine and parse entry point class / script, which is virtually undeterminable from the outside. Although easy for xp [script|file|class], would require deep understanding of subcommand argument -> entrypoint mapping!

This would revive the search for an IPC mechanism - but all that just for reusing the simple-to-implement Composer module logic? Doesn't seem worth it...

@thekid
Copy link
Member Author

thekid commented Oct 3, 2016

Standalone usecase

In a script or class with nothing alongside it, the dependencies should be loaded from Composer's global project (COMPOSER_HOME).

Usecase inside a project

Although typically not useful as the generated autoloader takes care of loading classes, we also want the following scenario to work along the principile of least surprise:

+ [app]
|- composer.json
|- class.pth               # Contains src/main/php, vendor/autoload.php
|- src
|  `- main
|     `- php
|        `- App.class.php  # Contains from() statements
`- vendor
   |- autoload.php
   `- ...

When running App.class.php, this should use the libraries in vendor instead of those from the global path. Also, since vendor/autoload.php takes care of class path setup, no modules need to be loaded!

@thekid
Copy link
Member Author

thekid commented Oct 3, 2016

The from statement causes problems in conjunction with use:

Api.class.php

<?php namespace de\thekid\shorturl;

from('xp-framework/scriptlet', 'xp\scriptlet\WebApplication');

class Api implements \xp\scriptlet\WebLayout {
  public function mappedApplications($profile= null) {
     $injector= new Injector(new Bindings());
     //                      ^^^^^^^^^^^^^^
     //                      Loads Bindings class - see below

     return ['/' => $injector->get(WebApplication::class)];
  }
}

Bindings.class.php

<?php namespace de\thekid\shorturl;

use xp\scriptlet\WebApplication;

class Bindings extends \inject\Bindings {

  public function configure($injector) {
    $injector->bind(WebApplication::class, new ShortUrlApplication());
  }
}

Result

Compile error: Cannot use xp\scriptlet\WebApplication as WebApplication because the name is already in use.

Hrm...

Unfortunately we cannot work around this - _maybe from should only be allowed inside scripts?_

@thekid
Copy link
Member Author

thekid commented Jan 14, 2017

maybe from should only be allowed inside scripts?

If so, we could create our own syntax, e.g.

use text\json\StreamInput from 'xp-forge/json';
use webservices\rest\{Endpoint, Links} from 'xp-framework/rest';

This is similar to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

@thekid
Copy link
Member Author

thekid commented Jan 15, 2017

Rewrote the RFC to use use ... from form instead of the originally proposed from() function:

// Original proposal, no longer works!!!
from('xp-forge/json', ['text\json\StreamInput']);
from('xp-forge/sequence', ['util\data\*']);
from('xp-framework/core', ['util\cmd\Console']);
from('xp-framework/logging', ['util\log\*']);
from('xp-framework/rest', ['webservices\rest\{Endpoint, Links}']);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant