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

How to use this to prefix a WordPress plugin? #442

Closed
Luc45 opened this issue Nov 27, 2020 · 7 comments
Closed

How to use this to prefix a WordPress plugin? #442

Luc45 opened this issue Nov 27, 2020 · 7 comments

Comments

@Luc45
Copy link

Luc45 commented Nov 27, 2020

Howdy!

I tried to use PHP-Scoper to prefix a WordPress plugin but couldn't. I saw the issue #303 and the PR #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
Copy link
Author

Luc45 commented Nov 30, 2020

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
Copy link
Author

Luc45 commented Dec 1, 2020

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 Luc45 closed this as completed Dec 1, 2020
@Luc45
Copy link
Author

Luc45 commented Dec 2, 2020

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 Luc45 reopened this Dec 2, 2020
@theofidry
Copy link
Member

@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

@swissspidy
Copy link

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/.

@leoloso
Copy link
Contributor

leoloso commented Jan 30, 2021

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/

@theofidry
Copy link
Member

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.

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

No branches or pull requests

4 participants