[go: nahoru, domu]

Iteration III: Packages (was Re: [PHP-DEV] [Initial Feedback] PHP User Modules - An Adaptation of ES6 from JavaScript)

From: Date: Sun, 30 Jun 2024 20:28:32 +0000
Subject: Iteration III: Packages (was Re: [PHP-DEV] [Initial Feedback] PHP User Modules - An Adaptation of ES6 from JavaScript)
References: 1 2 3 4 5 6 7 8 9 10  Groups: php.internals 
So let's take another crack at this based on all the points raised in the
thread. This should also underline why I don't consider this an RFC - I am
iterating until we arrive at something that may be refinable into an RFC.
And I say we because without the aid of those in this conversation I would
not have arrived at what will follow.

Before I continue I would like to apologize for being somewhat irritable.
We're all here because we enjoy using this language and want to see it
improved and prevent bad changes. Opinions will differ on this and in the
heat of the moment of arguing a point things can get borderline.


Returning to a point I made earlier, Composer isn't used on Wordpress.  I
went over to the Wordpress discussion list and read over why, because that
discussion provides clues to what kind of package management may be
adoptable. I think the largest point is that Wordpress should be usable
without ever resorting to using the command line. Yes, it does have a
command line tool - wp-cli - and it is powerful, but using it as an
administrator of a Wordpress site is not required.

The largest block to composer's inclusion in Wordpress is the inability to
run multiple versions of a module. Yes, it's a mess when this happens, but
if you're an end user, you just want your plugins to work.  If one plugin
that no one has updated in a year that you're using is consuming version 2
of a package, you're gonna be annoyed at best if the module stops working
when you install a new plugin that is using version 3 of the same package
and has a BC break in it.  Composer can't resolve this easily.

There are WordPress plugins that use composer - I have a couple in the
website I'm working on. But they accomplish the inclusion of composer by
redistributing the packages, and using a utility
called brianhenryie/strauss to monkey type the entire included package into
the plugin, changing the namespace of the entire package to something
different. The approach works, but it's ugly.  In any event, the plugin
that results from this carries a copy of the code from packagist rather
than sourcing the code from packagist.


-- IMPORT --

The import statement is for bringing in packages.  It needs to be able to
deal with:

* Extensions - the existing and oldest of packages for PHP
* PECL Extensions
* Phar Packages
* Composer Packages
* PHP Modules - this is the new module system that has dominated the
conversation, but in this iteration it's going to be broken away from
import to some degree in this iteration.

Today we'll look just at composer.

Now import needs to load packages in a manner that allows different
versions to be run concurrently. A PHP application such as Wordpress should
be distributable without needing to use the command line. That is, if
WordPress leverages this in any way, they don't have to give up their
famous 10 minute quick install.

Some terms here to keep myself from getting lost (let alone anyone trying
to read this).

* APPLICATION - This is the overall application - WordPress, Drupal,
BobbysFirstFramework, etc. - that is doing the import. This code is on the
root scope.
* ROOT SCOPE - This is where the global variables and the namespaces as we
know them exist.  Contrast this with
* PACKAGE SCOPE - Each package brought in with import gets its own package
scope. This is a distinct behavior from Include/Require. I think each
package scope will need to be on its own request thread, but this is an
implementation detail I can't speak to with any authority. The goal is
whatever happens in a package stays in the package.  If two different
packages want to define /foo(), they can.

When a package is imported the parser will look for a `.php.mod` file at
the root of the package. Among other things, this file details what type of
package it is and where to mount it by default in the namespace of the ROOT
SCOPE. So,

GIVEN a package with this .php.mod file

package MyModule


WHEN I issue this import in an application

import "MyModule";


THEN I should be able to access a method in that module with this code

\MyModule\foo();


Aliasing is an option - `import "MyModule" as BeerModule` will make the
methods accessible in the root with \BeerModule\foo();

Unlike require/include import is sensitive to the namespace it is in for
mounting.  So

namespace Trees;

import "MyModule";

MyModule\foo(); // works

\Trees\MyModule\foo(); // needed from another namespace.


That said, with aliasing an absolute namespace for the module can be
assigned.

namespace Trees;

import "MyModule" as \MyModule;

MyModule\foo(); // works if my understanding of existing namespace
resolution rules is correct.
\MyModule\foo(); // also works.


Now, with that in place, let's crack a tougher nut - handling a composer
package. By default composer is designed to set up an autoloader, then
resolve symbol references as they come up. This works until you have two
packages that want the same symbol reference - which will most frequently
occur with incompatible versions of the same package.  So our puzzle here
is how to allow composer to do its thing without rewriting it.  We'll deal
with admittedly the hardest case first - importing a package whose
maintainers have taken no action to make it compatible with this new system.



import "composer://packagist.org/packages/twig/twig#v3.10.3" as
TemplateEngine


The reason for that alias and not "Twig" is because the mounting point
comes before the internal namespace of the file. This is unavoidable with
this scheme

The URL there is "loader://package_url". PHP by default will know what the
composer loader is. It will look to see if the user has globally installed
composer already and use that, otherwise it will locally install composer
for the project, initialize it, download the package and have composer
resolve the package ending in setting up an autoloader that is only invoked
within that package.

Application configuration can make a lot of this go away.  So let's step
away from the import statement itself to look at that.


-- APPLICATIONS --
Applications can configure how they store their packages, but in the
absence of such PHP will use some logical default behaviors. We've already
looked at one, the loading of a composer package, but we have a long ugly
import as a result.

Most PHP applications have a single point of entry, and part of that is the
establishment of a cwd (current working directory). When PHP loads a file
it will look for a `.php-packages` directory in the current working
directory and if one doesn't exist it will make one the first time the
import statement is invoked (so code not using import will not have this
directory created). It is here that the package downloads land.  We can
also choose to go ahead and make this directory ourselves and place
`.php.mod` in that directory. Let's look at what one might look like for
Drupal, which already uses composer.


package Drupal;

php: 10


registry packagist.org/packages composer

imports (
  phar://getcomposer.org/composer.phar
)

init (
  composer install
)

require (
 ./vendor/autoload.php
)

Now, composer is a known and popular quantity, so the imports, init and
require directives can probably be baked into PHP, but if they ever change
- or if a competitor to composer shows up like yarn did to npm then there
needs to be a way to set it up.

Also, for the moment I'm using go.mod's format because it feels
the cleanest.  The exact format of this file - whether it's yaml, json,
toml, .ini or whatever else, is a discussion for another day. Key in on the
type of information that needs to be relayed here, not how it's relayed.

Importantly, because this .php.mod file is at the top level of the
application's .php-packages directory it affects the behavior of the ROOT
SCOPE.

The php directive gives the minimum php version for the application.

The registry directive sets the registry to packagist and sets the loader
for that registry to composer. Multiple registries can have separate
loaders.

The imports directive loads composer using the default phar loader. We use
an absolute path because we don't have a phar registry. This particular
call could be baked in due to composer's popularity.

The init directive runs the first time the application runs, just before
any file in the application is parsed.  The .php.sum file will bookmark the
last time the init has run and there will likely need to be a mechanism to
force it's rerun.

The require directive requires these files once before starting the
application's first file.

Assume we move our existing composer.json file into .php-packages, what
then?  We gain the following:
* Composer install will be ran for us, without using the cli.
* The autoloader will be set up for us without explicitly requiring it
anywhere.
* We can install an alternate version of packages into their own package
scope

import "twig/twig#v2.5" as OldTwig;


Or again, if we are in a namespace we can import to that namespace.

I'll stop here cause this is a healthy chunk to absorb and I just spent 4
hours thinking on this as I wrote it out and I'm tired. If this was in the
language today, Wordpress plugins could start using it without fear of
mucking each other up or messing with Wordpress core. But that's a
discussion for later.

« previous php.internals (#124115) next »