php-scoper icon indicating copy to clipboard operation
php-scoper copied to clipboard

How to use this to prefix a WordPress plugin?

Open Luc45 opened this issue 4 years ago • 6 comments

Howdy!

I tried to use PHP-Scoper to prefix a WordPress plugin but couldn't. I saw the issue https://github.com/humbug/php-scoper/issues/303 and the PR https://github.com/humbug/php-scoper/pull/433, I've read through a fair amount of comments and tried to get hands down with it myself.

I will list the steps I took to scope my WordPress plugin vendor folder, and the results that I've got:

Approach 1, per-dependency scope:

File: scoper-di52.php

php /var/www/tests/php-scoper/php-scoper.phar add-prefix --output-dir=/var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/lucatume/di52 --config=/var/www/tests/php-scoper/scoper-di52.php

Result: The prefixed di52 package in vendor_prefixed/lucatume/di52, as is expected. But there are a few issues with this approach:

  • The prefixed library is outside the Composer Autoloader, which means I would have to autoload it in a different way?
  • The original, unprefixed library, is still being loaded on the Composer Autoloader

Approach 2, scope the entire vendor folder:

File: scoper.php

php /var/www/tests/php-scoper/php-scoper.phar add-prefix --output-dir=/var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed --config=/var/www/tests/php-scoper/scoper.php

Result: All vendor dependencies prefixed in the vendor_prefixed folder, including Composer itself. This should work, but I had to add this to the scoper.php file, which raises an eyebrown for other exceptions that I might be missing and find out it can cause a fatal down the road:

'patchers' => [
    function (string $filePath, string $prefix, string $contents): string {
        // Change the contents here.

        if ($filePath === '/var/www/single/wp-content/plugins/wp-staging-dev/vendor/composer/autoload_real.php') {
            $contents = str_replace('\'Composer\\\\Autoload\\\\ClassLoader\'', '\'MyPlugin\\\\Vendor\\\\Composer\\\\Autoload\\\\ClassLoader\'', $contents);
            $contents = str_replace('\'Composer\\Autoload\\ClassLoader\'', '\'MyPlugin\\Vendor\\Composer\\Autoload\\ClassLoader\'', $contents);
        }

        return $contents;
    },
],

Patching is somewhat expected, but the fact that I had to do it in Composer Autoloader worries me, as the autoloader seems fragile as it's auto-generated.

With approach 1, I have added this to my composer.json so it can find the prefixed classes:

"autoload": {
  "psr-4": {
    "MyPlugin\\": "",
    "MyPlugin\\Vendor\\": "vendor_prefixed"
  }
}

I'm not sure if this would work well for attempt 2.

Am I taking the right approach to scope this WordPress plugin? Is there a third approach I should consider?

Thanks!

Luc45 avatar Nov 27 '20 18:11 Luc45

I see that Yoast and Google Site Kit uses PHP Scoper to prefix their WordPress plugins.

Both implementations seem to go through a fair amount of effort to implement their own autoloading solutions for the prefixed vendor classes.

Yoast

Prefixes the vendor classes, removes the unprefixed ones from Composer Autoloader using bash automation, then implement a spl_autoload_register to load the prefixed classes.

Google Site Kit

Takes a similar approach, implementing their own spl_autoload_register autoloader, and it seems it drops Composer autoloader entirely.

WooCommerce

Is taking the Mozart route.

Can PHP Scoper work with Composer autoloader? Would you recommend using PHP Scoper on a WordPress plugin?

Luc45 avatar Nov 30 '20 12:11 Luc45

I ended up figuring it out.

This approach is similar to Google Site Kit, generating a class map to avoid Composer doing file_exists checks on the filesystem.

The scoping process should happen as part of a build process of a distributable version of the plugin. not for development. Understand why here. (This approach uses the Optimization Level 1: Class map generation)

This is not mean to be a copy-and-work solution. In this example I use Docker to have predictable paths, you might need to adjust those to your reality. I also use the Phar version of PHP-Scoper.

Makefile:

php-scoper-docker:
	# Check run as root
	if ! [ "$(shell id -u)" = 0 ] ; then echo "This command MUST run as ROOT"; exit 1 ;fi

	# Check run inside Docker Container
	test -f /.dockerenv || { echo "This command MUST run inside the DOCKER PHP Container"; exit 1; }

	# Cleanup
	rm -rf /var/www/single/wp-content/plugins/wp-staging-dev/vendor
	rm -rf /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed

	# Install Dependencies
	composer install --no-dev --prefer-dist --no-scripts --working-dir=/var/www/single/wp-content/plugins/wp-staging-dev

	# Prepare prefixed vendor
	mkdir /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed
	cp /var/www/single/wp-content/plugins/wp-staging-dev/composer.json /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/composer.json
	cp /var/www/single/wp-content/plugins/wp-staging-dev/composer.lock /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/composer.lock

	# Add Prefix
	php /var/www/tests/php-scoper/php-scoper.phar add-prefix --output-dir=/var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/vendor --config=/var/www/tests/php-scoper/scoper.php --force

	# Generate prefixed vendor classmap
	composer dump-autoload --optimize --no-scripts --working-dir=/var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed

	# Build vendor again without dependencies
	cp /var/www/single/wp-content/plugins/wp-staging-dev/composer.json /var/www/single/wp-content/plugins/wp-staging-dev/composer.json.bk
	cp /var/www/single/wp-content/plugins/wp-staging-dev/composer.lock /var/www/single/wp-content/plugins/wp-staging-dev/composer.lock.bk

	composer show --direct --no-dev --name-only --working-dir=/var/www/single/wp-content/plugins/wp-staging-dev | xargs composer remove --no-scripts --working-dir=/var/www/single/wp-content/plugins/wp-staging-dev

	composer install --no-dev --prefer-dist -o --no-scripts --working-dir=/var/www/single/wp-content/plugins/wp-staging-dev

	# Revert vendor
	mv --force /var/www/single/wp-content/plugins/wp-staging-dev/composer.json.bk /var/www/single/wp-content/plugins/wp-staging-dev/composer.json
	mv --force /var/www/single/wp-content/plugins/wp-staging-dev/composer.lock.bk /var/www/single/wp-content/plugins/wp-staging-dev/composer.lock

	# Adjust vendor_prefixed paths
	rm -f /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/composer.json
	rm -f /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/composer.lock
	mv /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/vendor/* /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/
	rmdir /var/www/single/wp-content/plugins/wp-staging-dev/vendor_prefixed/vendor

	# Permissions fix for files created by root
	chown -R www-data:www-data /var/www/single/wp-content/plugins/wp-staging-dev

composer.json (Triggers a Makefile to run inside the Docker container):

"scripts": {
    "post-autoload-dump": "docker exec --user root -i wpstaging_php bash -c 'cd /var/www/tests && make php-scoper-docker'"
}

Loading the autoloader in the plugin (Inspired by Google Site Kit):

$class_map = array_merge(
    require_once __DIR__ . '/vendor/composer/autoload_classmap.php',
    require_once __DIR__ . '/vendor_prefixed/composer/autoload_classmap.php'
);

spl_autoload_register(
    function ($class) use ($class_map) {
        if (isset($class_map[$class])) {
            require_once $class_map[$class];

            return true;
        }

        return null;
    },
    true,
    true
);

'/vendor/composer/autoload_classmap.php' contains a classmap for the src code '/vendor_prefixed/composer/autoload_classmap.php' contains a classmap for the vendor_prefixed code

Check the contents of those two files to see if everything looks right to you.

Luc45 avatar Dec 01 '20 14:12 Luc45

Re-opening this as privately requested for visibility and further discussion around the subject.

Maintainers of this package, please feel free to close this if you see fit.

Luc45 avatar Dec 02 '20 12:12 Luc45

@Luc45 thanks for opening the issue; there is also #303. There is some ideas I hope to get to it in the coming weeks; It's a good idea to keep this issue as well to come back to it after I think

theofidry avatar Dec 07 '20 19:12 theofidry

Since you mentioned Site Kit by Google, you might also want to check out how we've done the prefixing in another Google plugin here: https://github.com/google/web-stories-wp/.

swissspidy avatar Dec 14 '20 16:12 swissspidy

I just published how I scoped my WordPress plugin: https://graphql-api.com/blog/graphql-api-for-wp-is-now-scoped-thanks-to-php-scoper/

leoloso avatar Jan 30 '21 17:01 leoloso

There is plenty of answers here so I'll be closing this issue. I am not satisfied with the WP support but I'll keep #303 for this purpose.

theofidry avatar Nov 13 '22 20:11 theofidry