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

using composer autoload and strauss autoload #34

Open
coder-at-heart opened this issue Sep 19, 2021 · 20 comments
Open

using composer autoload and strauss autoload #34

coder-at-heart opened this issue Sep 19, 2021 · 20 comments
Labels
bug Something isn't working documentation Improvements or additions to documentation v1.0

Comments

@coder-at-heart
Copy link

coder-at-heart commented Sep 19, 2021

After adding strauss, it seems that I need to use both the composer autoload and the strauss autoload.

include_once('strauss/autoload.php');
include_once('vendor/autoload.php');

The staruss process doesn't generate autoload data for my own classes / namespace.

  "autoload": {
    "psr-4": {
      "MyPlugin\\": "src/php"
    }
  },

Is there a setting I need add so that I only need to use one autoloader? Or am I missing something ?

Thanks

@BrianHenryIE
Copy link
Owner

What you're doing should work without issue. Strauss uses Composer's own tooling to create its autoload.php. But what you might prefer to do...

You can add the strauss directory to your Composer autoloader's classmap entry:

{
  "autoload": {
    "psr-4": {
      "MyPlugin\\": "src/php"
    },
    "classmap": ["strauss"]
  }
}

Then run composer dump-autoload and Composer will scan that directory and add the files to its autoloader.

A little problem now is you still have the old files in your project, so you can have Strauss delete them as it runs by setting delete_vendor_files to true in the extra/strauss key in your composer.json :

"extra": {
    "strauss": {
        "delete_vendor_files": true
    }
}

I'll leave this issue open until I add some verbose output to the end of vendor/bin/strauss to check if users have the output directory in their autoload key.

@BrianHenryIE BrianHenryIE added the documentation Improvements or additions to documentation label Sep 19, 2021
@BrianHenryIE
Copy link
Owner

I forgot to add, if you have your Strauss output directory already in your composer.json autoload key, then Strauss won't bother to create its autoload.php.

This won't handle files autoloaders (which is a minority of cases). I'll think about communicating that to users after the process is complete too.

@coder-at-heart
Copy link
Author

Thanks Brian.

Here's my composer.json

{
  "name": "lnk7/tracked-links",
  "description": "A Tracked Link Plugin",
  "keywords": [
    "framework",
    "wordpress",
    "plugin"
  ],
  "type": "project",
  "license": "MIT",
  "require": {
    "geniepress/framework": "^1.0",
    "hashids/hashids": "^4.0",
    "jakeasmith/http_build_url": "^1"
  },
  "autoload": {
    "psr-4": {
      "TrackedLinks\\": "src/php"
    },
    "classmap": [
      "strauss"
    ]
  },
  "extra": {
    "strauss": {
      "delete_vendor_files": true
    }
  },
  "minimum-stability": "dev",
  "prefer-stable": true,
  "scripts": {
    "strauss": [
      "@php -d memory_limit=-1 strauss.phar"
    ],
    "post-install-cmd": [
      "@strauss",
      "composer dumpautoload"
    ],
    "post-update-cmd": [
      "@strauss",
      "composer dumpautoload"
    ]
  }
}

I'm using the composer autoload...

include_once('vendor/autoload.php');

however I get

Warning: require(/Users/....../trackedlinks.local/wp-content/plugins/tracked-links/vendor/composer/../symfony/polyfill-php80/bootstrap.php): failed to open stream: No such file or directory in /Users/....../trackedlinks.local/wp-content/plugins/tracked-links/vendor/composer/autoload_real.php on line 71

It looks like the delete is removing some of the code that composer needs.

Any help appreciated.

@BrianHenryIE
Copy link
Owner

I guess this is a bug. The files from the files autoloaders are being moved, modified and the originals deleted (by design). When composer dump-autoload is run, it scans for classes, but doesn't refresh the entries from file autoloaders that it created before Strauss was run.

As I searched for a solution, I saw this comment from one of the Composer maintainers!

files autoloading was seen as more of a hack initially
composer/composer#10024 (comment)

I have a solution for you for today. A script that reads two of Composer's PHP files and does a string replace with the updated path. Save this in your project folder as update-files-autoload-directory.php

<?php
/**
 * @see https://github.com/BrianHenryIE/strauss/issues/34
 */
$straussOutputDir = 'strauss';

$autoloadFilesDotPhpTxt = file_get_contents( __DIR__ . '/vendor/composer/autoload_files.php' );
preg_match_all('/(\'\w*\' => )(\$vendorDir \. \')(.*)/', $autoloadFilesDotPhpTxt, $autoloadFilesMatches);

$autoloadStaticDotPhpTxt = file_get_contents( __DIR__ . '/vendor/composer/autoload_static.php' );
preg_match_all('/(\'\w*\' => )(__DIR__ \. \'\/\.\.\' \. \')(.*)/', $autoloadStaticDotPhpTxt, $autoloadStaticMatches);

$autoloadStaticReplacements = array();
foreach( $autoloadStaticMatches[0] as $index => $staticFileMatches ) {

	// `autoload_static.php` contains `files` autoloaders and more. ONLY operate on the files.
	// Check is it in the autoload_files file.
	if( in_array( $autoloadStaticMatches[3][$index], $autoloadFilesMatches[3] ) ) {
		$autoloadStaticReplacements[$staticFileMatches] = $autoloadStaticMatches[1][$index] . "__DIR__ .'/../../$straussOutputDir" . $autoloadStaticMatches[3][$index];
	}
}

$autoloadFilesReplacements = array();
foreach( $autoloadFilesMatches[0] as $index => $match ) {
	$autoloadFilesReplacements[$match] = $autoloadFilesMatches[1][$index] . "__DIR__ .'/../../$straussOutputDir" . $autoloadFilesMatches[3][$index];
}

// Make the changes:
foreach ($autoloadStaticReplacements as $search => $replace ) {
	$autoloadStaticDotPhpTxt = str_replace( $search, $replace, $autoloadStaticDotPhpTxt );
}
file_put_contents(__DIR__ . '/vendor/composer/autoload_static.php', $autoloadStaticDotPhpTxt);

foreach ($autoloadFilesReplacements as $search => $replace ) {
	$autoloadFilesDotPhpTxt = str_replace( $search, $replace, $autoloadFilesDotPhpTxt );
}
file_put_contents(__DIR__ . '/vendor/composer/autoload_files.php', $autoloadFilesDotPhpTxt);

I was testing this on geniepress/plugin. For some reason (another bug to investigate!) it needed these packages explicitly required:

composer require symfony/polyfill-php73
composer require react/promise

Then this should work

rm -rf strauss;
rm -rf vendor;
mkdir strauss;
composer install;
vendor/bin/strauss;
composer dump-autoload;

php update-files-autoload-directory.php;

And confirm no errors with:

php vendor/autoload.php

I'll have to think about how to get that working seamlessly. It looks like I can register Strauss as Composer Plugin, hook into pre-install-cmd or pre-autoload-dump and maybe manipulate the autoload keys there.

@BrianHenryIE BrianHenryIE added the bug Something isn't working label Sep 19, 2021
@BrianHenryIE
Copy link
Owner

BrianHenryIE commented Sep 19, 2021

Another, more straightforward solution!

I've created a branch in-situ for Strauss to edit the files inside the vendor folder without copying them first.

Your autoload classmap key needs vendor in it now, and Strauss config needs target_directory set to vendor:

  "autoload": {
    "psr-4": {
      "TrackedLinks\\": "src/php"
    },
    "classmap": [
      "vendor"
    ]
  },
  "extra": {
    "strauss": {
      "target_directory": "vendor"
    }
  },

Then tell Composer to download Strauss from the new branch and reinstall everything:

composer require --dev brianhenryie/strauss:dev-in-situ;

rm -rf strauss;
rm -rf vendor;
composer install;
vendor/bin/strauss;
composer dump-autoload;

php vendor/autoload.php;

This has just briefly been tested on geniepress/plugin, but it's been on my mind for a while to try. There's no reason it shouldn't work.

I'll test it myself over the next few weeks and get it merged and released.

@coder-at-heart
Copy link
Author

Hi Brian - sorry I missed these update - This is great . I managed to build my own autoloader for my own classes ( 5 lines of code) and include the strauss autoloader. works perfectly.

@robrecord
Copy link

Hi Brian,

Did this get merged? What is the proper way to do this in 2022? I'm having similar troubles as the OP (at least to my untrained eye, it could be something else)

@TimothyBJacobs
Copy link

Something to note if you use this approach, make sure to enable Authoritative Classmaps in composer. Otherwise, you may get "class already declared" errors if another plugin tries to load an unprefixed version of a class you also require. The classmap won't find the prefixed version, but the PSR autoloading rules will load the namespaced file. If your plugin loads first, then you'll end up with a duplicate classname error.

@pbowyer
Copy link

pbowyer commented Feb 20, 2023

Just wanted to leave an update for others who find this issue: this branch works brilliantly 😁

As @TimothyBJacobs says you need Authorative classmaps and I've included an example below. You also need to run composer dump-autoload after composer install (the @strauss calls in your composer.json scripts section aren't enough).

My composer.json

{
    "require": {
        "...insert your packages here...": "as usual. delete this line obviously"
    },
    "autoload": {
        "psr-4": {
            "PeterHas\\": "src/"
        },
        "classmap": [
            "vendor"
        ]
    },
    "require-dev": {
        "brianhenryie/strauss": "dev-in-situ"
    },
    "scripts": {
        "strauss": [
            "vendor/bin/strauss"
        ],
        "post-install-cmd": [
            "@strauss"
        ],
        "post-update-cmd": [
            "@strauss"
        ]
    },
    "extra": {
        "strauss": {
            "target_directory": "vendor",
            "namespace_prefix": "PeterHas\\Vendor\\",
            "classmap_prefix": "PeterHas_Vendor_",
            "constant_prefix": "PHV_",
            "exclude_from_prefix": {
                "packages": [
                ],
                "namespaces": [
                ],
                "file_patterns": [
                ]
            }
        }
    },
    "config": {
        "classmap-authoritative": true,
        "optimize-autoloader": true,
        "sort-packages": true
    }
}

And then

composer install
composer dump-autoload

Thanks @BrianHenryIE for this superb tool!

@pbowyer
Copy link

pbowyer commented Mar 7, 2023

I've encountered a bug (I think) using the configuration above if I install packages more than once.

{
    "require": {
        "monolog/monolog": "^3.2",
        "ruflin/elastica": "^7.3",
        "elasticsearch/elasticsearch": "^7.17",
        "symfony/console": "^5.4"
    },
    "autoload": {
        "psr-4": {
            "PicturingHistory\\": "src/"
        },
        "classmap": [
            "vendor"
        ]
    },
    "require-dev": {
        "brianhenryie/strauss": "dev-in-situ"
    },
    "scripts": {
        "strauss": [
            "vendor/bin/strauss"
        ],
        "post-install-cmd": [
            "@strauss"
        ],
        "post-update-cmd": [
            "@strauss"
        ]
    },
    "extra": {
        "strauss": {
            "target_directory": "vendor",
            "namespace_prefix": "PeterHas\\Vendor\\",
            "classmap_prefix": "PeterHas_Vendor_",
            "constant_prefix": "PHV_",
            "exclude_from_prefix": {
                "packages": [
                ],
                "namespaces": [
                ],
                "file_patterns": [
                ]
            }
        }
    },
    "config": {
        "classmap-authoritative": true,
        "optimize-autoloader": true,
        "sort-packages": true
    }
}

On the first run:

$ composer install && composer dump-autoload
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 42 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-php80 (v1.27.0): Extracting archive
  - Installing symfony/deprecation-contracts (v3.2.0): Extracting archive
  - Installing symfony/finder (v5.4.17): Extracting archive
  - Installing symfony/polyfill-mbstring (v1.27.0): Extracting archive
  - Installing symfony/polyfill-intl-normalizer (v1.27.0): Extracting archive
  - Installing symfony/polyfill-intl-grapheme (v1.27.0): Extracting archive
  - Installing symfony/polyfill-ctype (v1.27.0): Extracting archive
  - Installing symfony/string (v6.2.2): Extracting archive
  - Installing psr/container (2.0.2): Extracting archive
  - Installing symfony/service-contracts (v3.2.0): Extracting archive
  - Installing symfony/polyfill-php73 (v1.27.0): Extracting archive
  - Installing symfony/console (v5.4.17): Extracting archive
  - Installing league/mime-type-detection (1.11.0): Extracting archive
  - Installing league/flysystem (1.1.10): Extracting archive
  - Installing psr/simple-cache (1.0.1): Extracting archive
  - Installing psr/log (2.0.0): Extracting archive
  - Installing nikic/php-parser (v4.15.3): Extracting archive
  - Installing myclabs/php-enum (1.8.4): Extracting archive
  - Installing json-mapper/json-mapper (2.14.3): Extracting archive
  - Installing symfony/process (v6.2.0): Extracting archive
  - Installing symfony/polyfill-php81 (v1.27.0): Extracting archive
  - Installing symfony/filesystem (v6.2.0): Extracting archive
  - Installing seld/signal-handler (2.0.1): Extracting archive
  - Installing seld/phar-utils (1.2.1): Extracting archive
  - Installing seld/jsonlint (1.9.0): Extracting archive
  - Installing react/promise (v2.9.0): Extracting archive
  - Installing justinrainbow/json-schema (5.2.12): Extracting archive
  - Installing composer/pcre (3.1.0): Extracting archive
  - Installing composer/xdebug-handler (3.0.3): Extracting archive
  - Installing composer/spdx-licenses (1.5.7): Extracting archive
  - Installing composer/semver (3.3.2): Extracting archive
  - Installing composer/metadata-minifier (1.0.0): Extracting archive
  - Installing composer/class-map-generator (1.0.0): Extracting archive
  - Installing composer/ca-bundle (1.3.5): Extracting archive
  - Installing composer/composer (2.5.1): Extracting archive
  - Installing brianhenryie/strauss (dev-in-situ 82d0568): Extracting archive
  - Installing ezimuel/guzzlestreams (3.1.0): Extracting archive
  - Installing ezimuel/ringphp (1.2.2): Extracting archive
  - Installing monolog/monolog (3.2.0): Extracting archive
  - Installing nyholm/dsn (2.0.1): Extracting archive
  - Installing elasticsearch/elasticsearch (v7.17.1): Extracting archive
  - Installing ruflin/elastica (7.3.0): Extracting archive
Generating optimized autoload files
Warning: Ambiguous class resolution, "Composer\InstalledVersions" was found in both "/www/vendor/composer/composer/src/Composer/InstalledVersions.php" and "/www/vendor/composer/InstalledVersions.php", the first will be used.
29 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> vendor/bin/strauss
Generating optimized autoload files (authoritative)
Warning: Ambiguous class resolution, "Composer\Autoload\ClassLoader" was found in both "/www/vendor/composer/ClassLoader.php" and "/www/vendor/composer/composer/src/Composer/Autoload/ClassLoader.php", the first will be used.
Warning: Ambiguous class resolution, "Composer\InstalledVersions" was found in both "/www/vendor/composer/composer/src/Composer/InstalledVersions.php" and "/www/vendor/composer/InstalledVersions.php", the first will be used.
Generated optimized autoload files (authoritative) containing 2074 classes

So far so good. Let's run the command again:

$ composer install && composer dump-autoload
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Nothing to install, update or remove
Generating optimized autoload files
Warning: Ambiguous class resolution, "Composer\Autoload\ClassLoader" was found in both "/www/vendor/composer/ClassLoader.php" and "/www/vendor/composer/composer/src/Composer/Autoload/ClassLoader.php", the first will be used.
Warning: Ambiguous class resolution, "Composer\InstalledVersions" was found in both "/www/vendor/composer/composer/src/Composer/InstalledVersions.php" and "/www/vendor/composer/InstalledVersions.php", the first will be used.
29 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> vendor/bin/strauss
PHP Fatal error:  Uncaught Error: Class "Symfony\Component\Console\Application" not found in /www/vendor/brianhenryie/strauss/src/Console/Application.php:8
Stack trace:
#0 /www/vendor/composer/ClassLoader.php(582): include()
#1 /www/vendor/composer/ClassLoader.php(433): Composer\Autoload\{closure}('/var/www/html/h...')
#2 /www/vendor/brianhenryie/strauss/bin/strauss(27): Composer\Autoload\ClassLoader->loadClass('BrianHenryIE\\St...')
#3 /www/vendor/brianhenryie/strauss/bin/strauss(29): {closure}('0.8.1')
#4 /www/vendor/bin/strauss(120): include('/var/www/html/h...')
#5 {main}
  thrown in /www/vendor/brianhenryie/strauss/src/Console/Application.php on line 8

Fatal error: Uncaught Error: Class "Symfony\Component\Console\Application" not found in /www/vendor/brianhenryie/strauss/src/Console/Application.php:8
Stack trace:
#0 /www/vendor/composer/ClassLoader.php(582): include()
#1 /www/vendor/composer/ClassLoader.php(433): Composer\Autoload\{closure}('/var/www/html/h...')
#2 /www/vendor/brianhenryie/strauss/bin/strauss(27): Composer\Autoload\ClassLoader->loadClass('BrianHenryIE\\St...')
#3 /www/vendor/brianhenryie/strauss/bin/strauss(29): {closure}('0.8.1')
#4 /www/vendor/bin/strauss(120): include('/var/www/html/h...')
#5 {main}
  thrown in /www/vendor/brianhenryie/strauss/src/Console/Application.php on line 8
Script vendor/bin/strauss handling the strauss event returned with error code 255
Script @strauss was called via post-install-cmd

My understanding is that the paths within Strauss are not being rewritten this time, so it can't find the packages. However a diff shows nothing wrong:

$ git diff --no-index good bad
diff --git a/good/composer/installed.php b/bad/composer/installed.php
index 984933607..a2ea3573f 100644
--- a/good/composer/installed.php
+++ b/bad/composer/installed.php
@@ -241,8 +241,8 @@
         'psr/log-implementation' => array(
             'dev_requirement' => false,
             'provided' => array(
-                0 => '1.0|2.0',
-                1 => '3.0.0',
+                0 => '3.0.0',
+                1 => '1.0|2.0',
             ),
         ),
         'psr/simple-cache' => array(

Any ideas?

@pbowyer
Copy link

pbowyer commented Mar 7, 2023

@BrianHenryIE I've updated my comment with my composer.json

@BrianHenryIE
Copy link
Owner

Hey, here's half a fix. Basically, build the phar from this branch yourself and run it against a --no-dev Composer install.

git clone https://github.com/BrianHenryIE/strauss.git;
cd strauss;
git checkout in-situ;
composer install;
wget -O phar-composer.phar https://github.com/clue/phar-composer/releases/download/v1.2.0/phar-composer-1.2.0.phar
mkdir build
mv vendor build/vendor
mv src build/src
mv bin build/bin
mv composer.json build
php -d phar.readonly=off phar-composer.phar build ./build/
# Move the new .phar to your actual project directory
# Update your Composer script to run "@php strauss.phar"
composer install --no-dev

But...

When you run this twice, the namespaces get prefixed twice. I'll mark this as a bug and look into it later.

This additional configuration should work but isn't:

"namespace_replacement_patterns": {
  "~(PeterHas\\\\Vendor\\\\)(\\1)*?~": "PeterHas\\Vendor\\"
}

I think the match is correct, I'm not 100% sure why it's not working.

@BrianHenryIE
Copy link
Owner

I've merged the in-situ branch into master since it shouldn't impact anyone who doesn't explicitly set their target directory to vendor. I.e. the .phar on master should work for this now. The duplicating prefix presumably is still a bug.

I would like proper tests around this, particularly since it's doing some deep dive edits to Composer's own work, which I think most of us don't ever read. Example composer.jsons with error messages would be great.

It may be possible to hook into Composer's plugin actions and make this a very seamless setup.

@pbowyer
Copy link

pbowyer commented Mar 27, 2023

Thanks @BrianHenryIE. I'm back on this project today and tested using the Phar from the master branch which works great - but as you say on second run duplicates the namespace prefix. I haven't made a regex fix for that work yet either.

Edit: I got the regex to match by removing the doubling of the slashes on the first part:

"namespace_replacement_patterns": {
  "~(PeterHas\\Vendor\\)(\\1)*?~": "PeterHas\\Vendor\\"
}

Unfortunately whatever regex I try the resulting statement in the vendor files is namespace ;

@ManuDoni
Copy link

ManuDoni commented Apr 13, 2023

Hello, I did not understand if there is a configuration that lets me autoload the prefixed classes and my classes (using psr4), but not the unprefixed ones.
I mean, using the master branch and possibly not in a hacky way.

@arjen-mediasoep
Copy link

@BrianHenryIE Have you found any solution for the duplicate namespace problem?
I've tried the following, it works correctly the first time, but the second time it duplicates it, and the third time it removes the duplicated:

"namespace_replacement_patterns": {
  "~(Arjen\\\\Vendor\\\\)(\\1)*~": "Arjen\\Vendor\\"
}

@BrianHenryIE
Copy link
Owner

@arjen-mediasoep try the latest release, please, 0.16.0

The code I had written earlier had a stupid mistake where I got the argument order wrong in str_starts_with()! ccac6af

@arjen-mediasoep
Copy link

@BrianHenryIE It works! You're my hero!

@korridor
Copy link

@TimothyBJacobs Thanks for the tip with the authoritative class maps. I also had the same problem with the newer version 0.16.0. Can anybody in this issue explain to me why this is necessary? I read the code in the autoloader files from composer, with and without authoritative class maps, and I don't understand why not using authoritative class maps creates “class already declared” errors.

@korridor
Copy link

@TimothyBJacobs I'd like to use this with normal class maps (not authoritative). Do you know why this is causing “class already declared” errors? I'd be willing to help fix this, but I first need to understand why this is happening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working documentation Improvements or additions to documentation v1.0
Projects
None yet
Development

No branches or pull requests

8 participants