Using Composer packages in a WordPress plugin

5 Jan 2023

Tutorial

When working on a new WordPress plugin, we wanted to make use of existing composer packages in the plugin.

Unfortunately, in the WordPress ecosystem this isn’t straightforward.

Attempting to find a solution using AI

ChatGPT Answer about Composer usage

My conversation with ChatGPT

Me:

Describe to me the process of how to use a composer package in a WordPress plugin

ChatGPT:

To use a Composer package in a WordPress plugin, you will need to follow these steps:

  1. Install Composer on your system if you don’t already have it installed. You can download Composer from the official website: https://getcomposer.org/
  2. Create a composer.json file in the root directory of your WordPress plugin. This file will contain the dependencies for your plugin.
  3. In the composer.json file, specify the package you want to use by adding it to the require section. For example:
{
"require": {
"package/name": "version"
}
}
  1. Run the composer install command in the terminal to install the package and its dependencies. This will create a vendor directory in your plugin where the packages will be installed.
  2. Load the package in your plugin code by including the autoloader file generated by Composer. You can do this by adding the following line of code to your plugin’s main PHP file
require_once __DIR__ . '/vendor/autoload.php';
  1. You can now use the package in your plugin code by referring to it using its namespace. For example:
use Package\Name;
 
$package = new Name();

That’s it! You should now be able to use the Composer package in your WordPress plugin.

Me:

Should I use PHP-Scoper instead?

ChatGPT:

PHP-Scoper is a tool that can be used to “scoper” PHP packages, meaning that it can modify the namespace of a package and its dependencies to avoid conflicts with other packages. It can be useful in cases where you want to use a package that has not been namespaced, and you want to avoid potential conflicts with other packages in your project.

Using PHP-Scoper to scope a package is a two-step process:

  1. Install PHP-Scoper by running the following command:
composer require --dev php-scoper/php-scoper
  1. Run PHP-Scoper to scope the package by running the following command:
vendor/bin/php-scoper add-prefix --output-dir=scoped-libs

This will modify the package and its dependencies to have a unique namespace and place the modified code in the scoped-libs directory. You can then include the autoloader file from this directory in your plugin to use the scoped package.

Whether or not you should use PHP-Scoper in your WordPress plugin depends on your specific use case. If you are using a package that is not namespaced and you want to avoid potential conflicts with other packages, then using PHP-Scoper can be a good solution. However, if the package you are using is already namespaced and there is no risk of conflicts, then using PHP-Scoper may not be necessary.

If you have followed the AI space lately, you are probably familiar with OpenAI and ChatGPT at this point. You know the possibilities have exploded in the last month or so. And deservedly as well.

Open the above example conversation to see pretty accurate instructions on using a Composer package in a WordPress Plugin. If you also know the ChatGPT trained from text found out all over the internet, you can spot a problem foreshadowing in it.

Unfortunately, Composer and WordPress are not designed to work together in a simple fashion. People try to use Composer to make assembling WordPress, its plugins and themes automated or try using it as a package manager and autoloader inside an individual plugin.

We are focusing on the latter aspect here.

Problem using Composer inside a Plugin

You know the problem is Composer is designed in a way to be used once in a PHP application. One of the most important safeguards is to prevent using multiple versions for the same package and ensure any package is used only once.

When Plugin A includes a Composer package and Plugin B includes the same package with a different version, they cause problems for each other.

To answer the question “why?”, we need to look at how Object Oriented programming is implemented in PHP.

Practical OOP in PHP

To help developers with The Dependency Inversion Principle, PHP implemented namespaces and autoloader methods at a language level. Also, the community came up with an autoloading standard. Alas, the very first PHP Standard Recommendation (PSR) was about autoloader (PSR-0, since superseded by PSR-4).

The Composer is implemented on top of those works that are convenient to use in both development and production environments. Also, introduced a packaging system for packages written in PHP.

Every package that uses Packagist uses the same file naming and namespacing convention, which makes it possible to autoload it uniformly.

What happens when a PHP application tries to autoload the same package from multiple sources?

Imagine Plugin A using monolog/monolog version 1 and Plugin B using monolog/monolog version 3 using Composer.

The register_autoload() is called two times and registers the same class names from Plugin A and Plugin B. But, because autoloading always loads the first result, it will only serve monolog/monolog version 1 from Plugin A. When Plugin B tries to use a function from version 3, the whole application crashes, bringing down the entire WordPress.

Enter Scoping

In the above example, both plugins used the same package. Therefore namespace, class names and file names were identical.

But does not need to be. For example, we can create a non-conflicted copy if we modify the namespace of every class in a package. Then, we can automatically load those classes without worrying about conflict. In that case, we can make the namespace modification automated as well. This is the premise of the PHP-Scoper tool.

CopyCraft Example

In our new WordPress plugin, we wanted to use our own OpenAi Client Library. To avoid any potential conflict with other plugins, we will scope it into our own unique namespace.

Because we are not using Composer for autoloading, we scope one dependency at a time. Here is a script to scope all used packages in one step:

#!/usr/bin/env bash
 
current=$PWD
dependencies=('art4/requests-psr18-adapter' 'clue/stream-filter' 'league/container' 'nyholm/psr7' 'php-http/discovery' 'php-http/message' 'php-http/message-factory' 'php-http/multipart-stream-builder' 'psr/container' 'psr/http-client' 'psr/http-message' 'spatie/data-transfer-object' 'tectalic/openai')
 
namespaces=('Art4\Requests' 'Clue\StreamFilter' 'League\Container' 'Nyholm\Psr7' 'Http\Discovery' 'Http\Message' 'Http\Message\Factory' 'Http\Message\MultipartStream' 'Psr\Container' 'Psr\Http\Client' 'Psr\Http\Message' 'Spatie\DataTransferObject' 'Tectalic\OpenAi')
 
for ((i = 0; i < ${#dependencies[@]}; ++i)); do
output_dir="$current/includes/Vendor/${namespaces[$i]//\\/\/}"
php-scoper add-prefix \
--config="$current/scoper.php" \
--force \
--quiet \
--output-dir="$output_dir" \
--prefix="OM4\CopyCraft\Vendor" \
--working-dir="vendor/${dependencies[$i]}/src"
done
 
# Fix for Http\Message\Factory
mv "$current"/includes/Vendor/Http/Message/Factory/* "$current"/includes/Vendor/Http/Message/
rmdir "$current"/includes/Vendor/Http/Message/Factory
 
# Special handling of psr/http-factory independently
php-scoper add-prefix \
--no-config \
--force \
--quiet \
--output-dir="$current/includes/Vendor/Psr/Http/Factory" \
--prefix="OM4\CopyCraft\Vendor" \
--working-dir="vendor/psr/http-factory/src"
 
mv "$current"/includes/Vendor/Psr/Http/Factory/* "$current"/includes/Vendor/Psr/Http/Message/
rmdir "$current"/includes/Vendor/Psr/Http/Factory
 
# Special handling of v1-compat inside art4/requests-psr18-adapter
php-scoper add-prefix \
--no-config \
--force \
--quiet \
--output-dir="$current/includes/Vendor/WpOrg/Requests" \
--prefix="OM4\CopyCraft\Vendor" \
--working-dir="vendor/art4/requests-psr18-adapter/v1-compat"
 
# remove autoload file
rm "$current"/includes/Vendor/WpOrg/Requests/autoload.php
 
# move Invalidargument to a PSR-4 autoladable location
mkdir "$current"/includes/Vendor/WpOrg/Requests/Exception
mv "$current"/includes/Vendor/WpOrg/Requests/InvalidArgument.php "$current"/includes/Vendor/WpOrg/Requests/Exception/InvalidArgument.php

As well as the corresponding scoper.php configuration file:

<?php
 
return array(
'patchers' => array(
// Handle Tectalic/OpenAi package.
function ( string $file_path, string $prefix, string $content ): string {
// Ensure PHPdoc comments for Models are prefixed correctly.
if ( ! \str_contains( $file_path, 'vendor/tectalic/openai/' ) ) {
return $content;
}
 
return str_replace(
' \Tectalic\OpenAi\Models\\',
" \\$prefix\Tectalic\OpenAi\Models\\",
$content
);
},
// Handle art4/requests-psr18-adapter package.
function ( string $file_path, string $prefix, string $content ): string {
// Ensure art4/requests-psr18-adapter uses the Requests library that is bundled with WordPress core.
if ( ! \str_contains( $file_path, 'vendor/art4/' ) ) {
return $content;
}
 
return str_replace(
'OM4\CopyCraft\Vendor\WpOrg\Requests\\',
'WpOrg\Requests\\',
$content
);
},
// Handle v1-compat inside art4/requests-psr18-adapter package.
function ( string $file_path, string $prefix, string $content ): string {
// Ensure art4/requests-psr18-adapter uses the Requests library that is bundled with WordPress core.
if ( ! \str_contains( $file_path, 'vendor/art4/' ) ) {
return $content;
}
 
return str_replace(
array(
'WpOrg\Requests\Exception\InvalidArgument',
'WpOrg\Requests\Post',
),
array(
'OM4\CopyCraft\Vendor\WpOrg\Requests\Exception\InvalidArgument',
'OM4\CopyCraft\Vendor\WpOrg\Requests\Post',
),
$content
);
},
),
);

After that, we only needed to add our own autoload.php file, and require it from our main plugin file (copycraft.php).

You can see the final result in version 0.1 of the CopyCraft plugin.

We’ve been using this kind of scoping solution in WooCommerce Zapier since version 2, and it has worked very well for us.