Skip to content

Commit

Permalink
feat: introduce php-scoper to scope our complete plugin dependencies …
Browse files Browse the repository at this point in the history
…(#4jnk84)
  • Loading branch information
matzeeable committed Apr 13, 2020
1 parent 1e985dc commit c9513b3
Show file tree
Hide file tree
Showing 21 changed files with 551 additions and 29 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ _Avoid repetitive work and develop more feature_
- Plugin creation with **monorepo integration**: `create-wp-react-app create-plugin`
- Package creation with **monorepo integration**: `create-wp-react-app create-package`
- Predefined [**GitLab CI**](https://about.gitlab.com/product/continuous-integration/) example for **Continous Integration** ([read more](./#using-ci-cd))
- [**Scoping**](https://github.com/humbug/php-scoper) your PHP coding and dependencies so they are isolated (avoid dependency version conflicts)
- **Packaging and publishing** of you plugin [wordpress.org](https://wordpress.org/plugins/developers/) ([read more](https://devowlio.gitbook.io/wp-react-starter/gitlab-integration/deploy-wp-org))
- [**license-checker**](https://www.npmjs.com/package/license-checker) for automated **3th-party-code license scanning** and compliance check

Expand All @@ -88,6 +89,7 @@ _Providing the right development environment for high quality plugins_
- [**lerna**](https://lerna.js.org/) for **semantic versioning** and **changelog generation**
- [**webpackbar**](https://github.com/nuxt/webpackbar) so you can get a real progress bar while development
- [**Docker**](https://www.docker.com/) for a **local development** environment
- Predefined WordPress **Stubs** so you get autocompletion for WordPress classes and functions, e. g. `add_action`
- Within the Docker environment you have [**WP-CLI**](https://developer.wordpress.org/cli/commands/) available
- Predefined [**Review Apps**](https://docs.gitlab.com/ee/ci/review_apps/) example for branch deployment, read more [here](./#using-ci-cd)
- Predefined VSCode **PHP debugging** environment
Expand Down
71 changes: 70 additions & 1 deletion common/Gruntfile.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
*/

import { execSync, spawnSync } from "child_process";
import { resolve, dirname } from "path";
import { resolve, dirname, basename } from "path";
import { renameSync } from "fs";
import { readFileSync, writeFileSync, lstatSync } from "fs";
import { applyDefaultRunnerConfiguration, hookable } from "./Gruntfile";
import rimraf from "rimraf";
import { extractGlobalStubIdentifiers } from "./php-scope-stub";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const mainPkg = require("../package.json");
Expand Down Expand Up @@ -250,6 +251,73 @@ function applyPluginRunnerConfiguration(grunt: IGrunt) {
grunt.file.delete(`${buildPluginDir}/wordpress.org`);
});

/**
* Scope our PHP plugin. See also php-scoper.php.
*
* @see https://github.com/humbug/php-scoper
*/
grunt.registerTask("php:scope", () => {
const cwd = process.cwd();
const buildPluginDir = resolve(grunt.config.get<string>("BUILD_PLUGIN_DIR"));
const configFile = resolve("../../common/php-scoper.php");
const outputDir = `${buildPluginDir}-scoped`;
const tmpStubFile = resolve(buildPluginDir, "php-scoper.php.json");

// Whitelist stubs, write them to a temporary file so php-scoper.php can consume it
const stubPathes = grunt.config.get<string[]>("pkg.stubs").map((relative) => resolve(cwd, relative));
const addOnPathes = grunt.file
.expand(
{
cwd: resolve("../")
},
["*/src/inc/**/*.php", `!${basename(cwd)}/**/*`]
)
.map((relative) => resolve("..", relative));
const whitelist = extractGlobalStubIdentifiers(stubPathes.concat(addOnPathes));
writeFileSync(tmpStubFile, JSON.stringify(whitelist), {
encoding: "UTF-8"
});

// Execute the php-scoper
spawnSync(`php-scoper add-prefix --output-dir="${outputDir}" --config "${configFile}"`, {
cwd: buildPluginDir,
stdio: "inherit",
shell: true
});

// Overwrite back all scoped files to main directory
grunt.file
.expand(
{
cwd: outputDir,
filter: "isFile"
},
"**/*"
)
.forEach((relative) => renameSync(resolve(outputDir, relative), resolve(buildPluginDir, relative)));

// It is essential to reload the autoloader files
const rebuildAutoloader = (cwd: string) =>
spawnSync(`composer dump-autoload --classmap-authoritative`, {
cwd,
stdio: "inherit",
shell: true
});
grunt.file
.expand(
{
cwd: buildPluginDir,
filter: "isDirectory"
},
"vendor/*/*"
)
.forEach((folder) => rebuildAutoloader(resolve(buildPluginDir, folder)));
rebuildAutoloader(buildPluginDir);

rimraf.sync(outputDir);
rimraf.sync(tmpStubFile);
});

/**
* Build the whole plugin to the distribution files.
*/
Expand All @@ -274,6 +342,7 @@ function applyPluginRunnerConfiguration(grunt: IGrunt) {
"composer:clean:production",
"clean:productionSource",
"strip_code:productionSource",
"php:scope",
"clean:packageManageFiles"
].concat(grunt.config.get("BUILD_POST_TASKS") || [])
)
Expand Down
65 changes: 65 additions & 0 deletions common/php-scope-stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { readFileSync } from "fs";
import phpParser from "php-parser";

const ALLOWED = ["class", "interface", "function", "trait"];

function findKinds(obj: any, key: string, namespace = "") {
let list: any[] = [];
if (!obj) return list;
if (obj instanceof Array) {
for (const i in obj) {
list = list.concat(findKinds(obj[i], key, namespace));
}
return list;
}

if (ALLOWED.indexOf(obj[key]) > -1 && obj.name) list.push({ ...obj.name, inNamespace: namespace });

if (typeof obj == "object" && obj !== null) {
const children = Object.keys(obj);
if (children.length > 0) {
// Correctly set namespace for next children
const appendNamespace =
obj.kind === "namespace" && typeof obj.name === "string"
? `${obj.name.split("\\").filter(Boolean).join("\\")}\\`
: namespace;
for (let i = 0; i < children.length; i++) {
list = list.concat(findKinds(obj[children[i]], key, appendNamespace));
}
}
}
return list;
}

/**
* Due to the fact that php-scoper does not support external global dependencies like WordPress
* functions and classes we need to whitelist them. The best approach is to use the already
* used stubs. Stubs are needed for PHP Intellisense so they need to be up2date, too.
*
* @see https://github.com/humbug/php-scoper/issues/303
* @see https://github.com/humbug/php-scoper/issues/378
*/
function extractGlobalStubIdentifiers(files: string[]) {
const result: string[] = [];

const parser = new phpParser({
parser: {
extractDoc: true
},
ast: {
withSource: false,
withPositions: false
}
});

for (const file of files) {
const parsed = parser.parseCode(readFileSync(file, { encoding: "UTF-8" }));
result.push(
...findKinds(parsed, "kind").map((id: { name: string; inNamespace: string }) => id.inNamespace + id.name)
);
}

return result;
}

export { extractGlobalStubIdentifiers };
59 changes: 59 additions & 0 deletions common/php-scoper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);

use Isolated\Symfony\Component\Finder\Finder;
//use Symfony\Component\Finder\Finder;

// Obtain original namespace (must be the first entry in autoload.psr-4)
$composerJson = json_decode(file_get_contents('composer.json'), true);
$psr4 = array_keys($composerJson['autoload']['psr-4'])[0];

// Obtain stubs generated by grunt task "php:scope"
$stubsJson = json_decode(file_get_contents('php-scoper.php.json'), true);

// Whitelist all available monorepo-plugins
$whiteListPlugins = glob('../../../*', GLOB_ONLYDIR);
foreach ($whiteListPlugins as $key => $plugin) {
$composerJson = json_decode(file_get_contents($plugin . '/composer.json'), true);
$whiteListPlugins[$key] = array_keys($composerJson['autoload']['psr-4'])[0] . '*';
}

return [
'prefix' => $psr4 . 'Vendor',
'finders' => [
Finder::create()
->files()
->in('inc'),
Finder::create()
->files()
->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
->exclude(['doc', 'test', 'test_old', 'tests', 'Tests', 'vendor-bin'])
->in('vendor'),
Finder::create()->append(['composer.json'])
],
'patchers' => [
function ($filePath, $prefix, $content) use ($stubsJson) {
$prefixDoubleSlashed = str_replace('\\', '\\\\', $prefix);
$quotes = ['\'', '"', '`'];

foreach ($stubsJson as $identifier) {
$identifierDoubleSlashed = str_replace('\\', '\\\\', $identifier);
$content = str_replace($prefix . '\\' . $identifier, $identifier, $content); // "PREFIX\foo()", or "foo extends nativeClass"

// Replace in strings, e. g. "if( function_exists('PREFIX\\foo') )"
foreach ($quotes as $quote) {
$content = str_replace(
$quote . $prefixDoubleSlashed . '\\\\' . $identifierDoubleSlashed . $quote,
$quote . $identifierDoubleSlashed . $quote,
$content
);
}
}
return $content;
}
],
'whitelist' => $whiteListPlugins,
'whitelist-global-constants' => true,
'whitelist-global-classes' => false,
'whitelist-global-functions' => false
];
3 changes: 3 additions & 0 deletions common/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,7 @@
<rule ref="Squiz.Commenting.FunctionCommentThrowTag.Missing">
<exclude-pattern>test/*</exclude-pattern>
</rule>
<!-- Slevomat -->
<rule ref="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly" />
<rule ref="SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash" />
</ruleset>
5 changes: 5 additions & 0 deletions common/stubs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
namespace {
// Put your global namespaces here.
// Learn more about it here: https://www.php.net/manual/en/language.namespaces.definitionmultiple.php
}
6 changes: 5 additions & 1 deletion devops/docker/gitlab-ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ RUN apt-get update && apt-get install -y \
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
php wp-cli.phar --info && \
chmod +x wp-cli.phar && \
mv wp-cli.phar /usr/local/bin/wp
mv wp-cli.phar /usr/local/bin/wp && \
# PHP Scoper
curl -O -L https://github.com/humbug/php-scoper/releases/download/0.13.1/php-scoper.phar && \
chmod +x php-scoper.phar && \
mv php-scoper.phar /usr/local/bin/php-scoper

RUN npm install -g \
# Global installations of npm packages
Expand Down
77 changes: 77 additions & 0 deletions docs/php-development/add-classes-hooks-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,80 @@ Before adding new dependencies, you should determine if its a development depend
{% endhint %}

{% page-ref page="../advanced/create-package.md" %}

## Stubs

### Stubs, what?

> STUBS are normal, syntactically correct PHP files that contain function & class signatures, constant definitions, etc. for all built-in PHP stuff and most standard extensions. Stubs need to include complete PHPDOC, especially proper @return annotations - [quote phpstorm-stubs](https://github.com/JetBrains/phpstorm-stubs#phpstorm-stubs).
But stubs can not be only used for PHP built-in functions and classes, they can also be generated for WordPress or created yourself. Scenario WordPress: A generator simply consumes the complete WordPress core coding, extracts all functions and classes and removes the implementation itself. The result is a file containing all definitions with documentation.

If you are more familiar with the JavaScript ecosystem: `stubs = TypeScript typings`.

### Why do I need stubs?

> An IDE needs them for completion, code inspection, type inference, doc popups, etc. Quality of most of these services depend on the quality of the stubs (basically their PHPDOC @annotations) - [quote phpstorm-stubs](https://github.com/JetBrains/phpstorm-stubs#phpstorm-stubs).
Imagine you code inside a project with VSCode and you use an external dependency, but you do not have the coding of the dependency itself - you will never obtain intellisense / autocompletion. In our case it's WordPress: We are coding our plugins and packages within a monorepo but the WordPress core coding is never in our repository. Gladly, WordPress provides a stub for their complete codebase: [wordpress-stubs](https://github.com/php-stubs/wordpress-stubs)

**wordpress-stubs is a predefined dependency of WP React Starter, you do not need to add it manually!**

### Add stubs

You have two options to add stubs to your plugin. No matter which way you take, the most important thing is that you add your stub to `package.json#stubs` so it is also considered in build process:

```json
{
"stubs": ["./vendor/php-stubs/wordpress-stubs/wordpress-stubs.php", "./scripts/stubs.php"]
}
```

{% hint style="info" %}
The build process uses [PHP-Scoper](https://github.com/humbug/php-scoper), it prefixes all PHP namespaces in a file/directory to isolate the code bundled. We can only point out how important it is to properly integrate stubs.
{% endhint %}

#### Define stubs yourself

Add a file `plugins/your-plugin/scripts/stubs.php` and put your stubs inside it. It can e. g. look like this:

```php
<?php

/**
* Set the sort order of a term.
*
* @param int $term_id Term ID.
* @param int $index Index.
* @param string $taxonomy Taxonomy.
* @param bool $recursive Recursive (default: false).
* @return int
*/
function wc_set_term_order($term_id, $index, $taxonomy, $recursive = false)
{
// Silence is golden.
}
```

{% hint style="info" %}
In `common/stubs.php` you will find a common stubs file which you can modify for all your plugins. The above example shows per-plugin stubbing.
{% endhint %}

#### As dependency

The best use case for this is if you are developing a plugin for WooCommerce and want to use WooCommerce functionality inside your plugin coding. WooCommerce is great and also offers a stub via [composer](https://packagist.org/packages/php-stubs/woocommerce-stubs). Navigate to your plugins folder and execute:

```bash
composer require --dev php-stubs/woocommerce-stubs
```

Do not forget to add the stub to your `package.json#stubs` as mentioned above:

```json
{
"stubs": [
"./vendor/php-stubs/wordpress-stubs/wordpress-stubs.php",
"./vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php"
]
}
```
1 change: 1 addition & 0 deletions docs/usage/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Please make sure that the following components are installed on your local computer:

- [**PHP**](https://www.php.net/manual/en/install.unix.debian.php): PHP runtime (version >= 7.2)
- [**Node.js**](https://nodejs.org/): JavaScript runtime (version >= 10.17)
- [**Yarn**](https://yarnpkg.com/lang/en/): Dependency manager for JavaScript (version >= 1.19)
- [**Composer**](https://getcomposer.org/): Dependency manager for PHP (version >= 1.9)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"lint-staged": "^10.0.9",
"parallel-webpack": "^2.4.0",
"patch-package": "^6.2.1",
"php-parser": "^3.0.0",
"postinstall-postinstall": "^2.0.0",
"prettier": "^2.0.2",
"rimraf": "^3.0.2",
Expand Down
7 changes: 6 additions & 1 deletion packages/utils/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@
},
"autoload": {
"psr-4": {
"MatthiasWeb\\Utils\\": "src/",
"MatthiasWeb\\Utils\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MatthiasWeb\\Utils\\Test\\": "test/phpunit/"
}
},
Expand All @@ -71,6 +75,7 @@
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7",
"rregeer/phpunit-coverage-check": "^0.3.1",
"slevomat/coding-standard": "^6.0@dev",
"squizlabs/php_codesniffer": "^3.5",
"wp-coding-standards/wpcs": "^2.2"
}
Expand Down
Loading

0 comments on commit c9513b3

Please sign in to comment.