Skip to content

Commit

Permalink
wp-now: create php command to execute PHP files (#336)
Browse files Browse the repository at this point in the history
- Related to
#242

## Proposed changes

- Create a new `php` command to execute php files
- Tests for `executePHPFile`

`wp-cli` will have its separate command in a different PR.

## Testing instructions

- Create a simple php. e.g. `example.php`: `<?php echo "Hello World!";`
- Copy the relative or absolute path to that file
- Execute `npx nx preview wp-now php ~/path-to/example.php`
- Modify your `example.php` to add WordPress functions: e.g. `<?php echo
get_bloginfo();`
- Execute `npx nx preview wp-now php ~/path-to/example.php
--path=/path-to-your-plugin/gutenberg` // To load WordPress needs to be
executed in a mode different than `index`.

---------

Co-authored-by: Daniel Bachhuber <[email protected]>
  • Loading branch information
sejas and danielbachhuber authored May 18, 2023
1 parent 1a31776 commit a8ede03
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 39 deletions.
10 changes: 10 additions & 0 deletions packages/wp-now/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ Use the `--php=<version>` and `--wp=<version>` arguments to switch to different
wp-now start --wp=5.9 --php=7.4
```

Alternativaly to start the server, you can executes php files using the command `wp-now php <filePath>`:

```bash
cd wordpress-plugin-or-theme
wp-now php /path/to/php-file.php
```

### Automatic Modes

`wp-now` automatically operates in a few different modes. The selected mode depends on the directory in which it is executed:
Expand All @@ -48,6 +55,9 @@ wp-now start --wp=5.9 --php=7.4
- `--port=<port>`: the port number on which the server will listen. This is optional and if not provided, it will pick an open port number automatically. The default port number is set to `8881`(example of usage: `--port=3000`);
- `--wp=<version>`: the version of WordPress to use. This is optional and if not provided, it will use a default version. The default version is set to the [latest WordPress version](https://wordpress.org/download/releases/)(example usage: `--wp=5.8`)

`wp-now php <phpFilePath>` currently supports `--path`, `--php` and `--wp`.
It executes the php file loading the WordPress context when possible.

## Technical Details

- The `~/.wp-now` home directory is used to store the WP versions and the `wp-content` folders for projects using 'theme', 'plugin', and 'wp-content' modes. The path to `wp-content` directory for the 'plugin', 'theme', and 'wp-content' modes is `~/.wp-now/wp-content/${projectName}-${directoryHash}`.
Expand Down
2 changes: 2 additions & 0 deletions packages/wp-now/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface WPNowOptions {
projectPath?: string;
wpContentPath?: string;
wordPressVersion?: string;
numberOfPhpInstances?: number;
}

export const DEFAULT_OPTIONS: WPNowOptions = {
Expand All @@ -49,6 +50,7 @@ export const DEFAULT_OPTIONS: WPNowOptions = {
documentRoot: '/var/www/html',
projectPath: process.cwd(),
mode: WPNowMode.AUTO,
numberOfPhpInstances: 1,
};

export interface WPEnvOptions {
Expand Down
4 changes: 3 additions & 1 deletion packages/wp-now/src/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export async function downloadWordPress(
});
if (downloaded) {
fs.ensureDirSync(path.dirname(finalFolder));
fs.renameSync(path.join(tempFolder, 'wordpress'), finalFolder);
fs.moveSync(path.join(tempFolder, 'wordpress'), finalFolder, {
overwrite: true,
});
} else if (404 === statusCode) {
output?.log(
`WordPress ${wordPressVersion} not found. Check https://wordpress.org/download/releases/ for available versions.`
Expand Down
62 changes: 62 additions & 0 deletions packages/wp-now/src/execute-php-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import fs from 'fs';
import path from 'path';
import startWPNow from './wp-now';
import { WPNowOptions } from './config';
import { disableOutput } from './output';

const VFS_TMP_PATH = '/vfs-wp-now-tmp';
const VFS_PHP_FILE = path.join(VFS_TMP_PATH, 'parent.php');

/**
*
* Execute a PHP file given its path. For non index mode it loads WordPress context.
*
* @param {string} filePath - The path to the PHP file to be executed.
* @param {WPNowOptions} [options={}] - Optional configuration object for WPNow. Defaults to an empty object.
* @returns {Promise<{ name: string; status: 0; }>} - Returns a Promise that resolves to an object containing
* the exit name and status (0 for success).
* @throws {Error} - Throws an error if the specified file is not found or if an error occurs while executing the file.
*/
export async function executePHPFile(
filePath: string,
options: WPNowOptions = {}
) {
disableOutput();
const { phpInstances, options: wpNowOptions } = await startWPNow({
...options,
numberOfPhpInstances: 2,
});
const [, php] = phpInstances;

// check if filePath exists
const absoluteFilePath = path.resolve(filePath);
if (!fs.existsSync(absoluteFilePath)) {
throw new Error(`Could not open input file: ${absoluteFilePath}`);
}

let fileToExecute = absoluteFilePath;
if (wpNowOptions.mode !== 'index') {
// Load WordPress context for non index mode.
php.mkdirTree(VFS_TMP_PATH);
php.writeFile(
VFS_PHP_FILE,
`<?php
$_SERVER['HTTP_HOST'] = '${wpNowOptions.absoluteUrl}';
require_once '${path.join(wpNowOptions.documentRoot, 'wp-load.php')}';
require_once '${filePath}';
`
);
fileToExecute = VFS_PHP_FILE;
}

try {
php.useHostFilesystem();
await php.cli(['php', fileToExecute]);
} catch (resultOrError) {
const success =
resultOrError.name === 'ExitStatus' && resultOrError.status === 0;
if (!success) {
throw resultOrError;
}
}
}
10 changes: 9 additions & 1 deletion packages/wp-now/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@ function shouldOutput() {
return process.env.NODE_ENV !== 'test';
}

export const output = shouldOutput() ? console : null;
export let output = shouldOutput() ? console : null;

export function enableOutput() {
output = console;
}

export function disableOutput() {
output = null;
}
57 changes: 44 additions & 13 deletions packages/wp-now/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { portFinder } from './port-finder';
import { SupportedPHPVersion } from '@php-wasm/universal';
import getWpNowConfig from './config';
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
import { executePHPFile } from './execute-php-file';
import { output } from './output';

function startSpinner(message: string) {
Expand All @@ -19,6 +20,23 @@ function startSpinner(message: string) {
};
}

function commonParameters(yargs) {
return yargs
.option('path', {
describe:
'Path to the PHP or WordPress project. Defaults to the current working directory.',
type: 'string',
})
.option('php', {
describe: 'PHP version to use.',
type: 'string',
})
.option('wp', {
describe: "WordPress version to use: e.g. '--wp=6.2'",
type: 'string',
});
}

export async function runCli() {
return yargs(hideBin(process.argv))
.scriptName('wp-now')
Expand All @@ -27,23 +45,11 @@ export async function runCli() {
'start',
'Start the server',
(yargs) => {
commonParameters(yargs);
yargs.option('port', {
describe: 'Server port',
type: 'number',
});
yargs.option('path', {
describe:
'Path to the PHP or WordPress project. Defaults to the current working directory.',
type: 'string',
});
yargs.option('php', {
describe: 'PHP version to use.',
type: 'string',
});
yargs.option('wp', {
describe: "WordPress version to use: e.g. '--wp=6.2'",
type: 'string',
});
},
async (argv) => {
const spinner = startSpinner('Starting the server...');
Expand All @@ -67,6 +73,31 @@ export async function runCli() {
}
}
)
.command(
'php <filePath>',
'Run the php command passing the arguments for php cli',
(yargs) => {
commonParameters(yargs);
yargs.positional('filePath', {
describe: 'Path to the PHP file to run',
type: 'string',
});
},
async (argv) => {
try {
const options = await getWpNowConfig({
path: argv.path as string,
php: argv.php as SupportedPHPVersion,
wp: argv.wp as string,
});
await executePHPFile(argv.filePath as string, options);
process.exit(0);
} catch (error) {
console.error(error);
process.exit(error.status || -1);
}
}
)
.demandCommand(1, 'You must provide a valid command')
.help()
.alias('h', 'help')
Expand Down
47 changes: 47 additions & 0 deletions packages/wp-now/src/tests/execute-php-file.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from 'fs-extra';
import path from 'path';
import { executePHPFile } from '../execute-php-file';
import getWpNowConfig from '../config';

const exampleDir = path.join(__dirname, 'execute-php-file');

test('php file execution in index mode', async () => {
const resultFilePath = path.join(exampleDir, 'hello-world-result.txt');
// reset result file
fs.writeFileSync(resultFilePath, '');
const options = await getWpNowConfig({
path: exampleDir,
});
await executePHPFile(path.join(exampleDir, 'hello-world.php'), options);
const output = fs.readFileSync(resultFilePath, 'utf8');
expect(output).toBe('Hello World!');
});

test('php file execution for each PHP Version', async () => {
const resultFilePath = path.join(exampleDir, 'php-version-result.txt');
const options = await getWpNowConfig({
path: exampleDir,
});
await executePHPFile(path.join(exampleDir, 'php-version.php'), {
...options,
phpVersion: '7.4',
});
let output = fs.readFileSync(resultFilePath, 'utf8');
expect(output.substring(0, 16)).toBe('PHP Version: 7.4');

await executePHPFile(path.join(exampleDir, 'php-version.php'), {
...options,
phpVersion: '8.0',
});
output = fs.readFileSync(resultFilePath, 'utf8');
expect(output.substring(0, 16)).toBe('PHP Version: 8.0');

await executePHPFile(path.join(exampleDir, 'php-version.php'), {
...options,
phpVersion: '8.2',
});
output = fs.readFileSync(resultFilePath, 'utf8');
expect(output.substring(0, 16)).toBe('PHP Version: 8.2');

fs.writeFileSync(resultFilePath, 'PHP Version: X.Y');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World!
4 changes: 4 additions & 0 deletions packages/wp-now/src/tests/execute-php-file/hello-world.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
$resultFile = fopen(__DIR__ . '/hello-world-result.txt', 'w');
fwrite($resultFile, 'Hello World!');
fclose($resultFile);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PHP Version: X.Y
4 changes: 4 additions & 0 deletions packages/wp-now/src/tests/execute-php-file/php-version.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
$resultFile = fopen(__DIR__ . '/php-version-result.txt', 'w');
fwrite($resultFile, "PHP Version: " . phpversion());
fclose($resultFile);
74 changes: 50 additions & 24 deletions packages/wp-now/src/wp-now.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ function seemsLikeAPHPFile(path) {
return path.endsWith('.php') || path.includes('.php/');
}

async function applyToInstances(phpInstances: NodePHP[], callback: Function) {
for (let i = 0; i < phpInstances.length; i++) {
await callback(phpInstances[i]);
}
}

export default async function startWPNow(
options: Partial<WPNowOptions> = {}
): Promise<{ php: NodePHP; options: WPNowOptions }> {
): Promise<{ php: NodePHP; phpInstances: NodePHP[]; options: WPNowOptions }> {
const { documentRoot } = options;
const php = await NodePHP.load(options.phpVersion, {
const nodePHPOptions = {
requestHandler: {
documentRoot,
absoluteUrl: options.absoluteUrl,
Expand All @@ -53,40 +59,59 @@ export default async function startWPNow(
}
},
},
};

const phpInstances = [];
for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) {
phpInstances.push(
await NodePHP.load(options.phpVersion, nodePHPOptions)
);
}
const php = phpInstances[0];

phpInstances.forEach((_php) => {
_php.mkdirTree(documentRoot);
_php.chdir(documentRoot);
_php.writeFile(
`${documentRoot}/index.php`,
`<?php echo 'Hello wp-now!';`
);
});
php.mkdirTree(documentRoot);
php.chdir(documentRoot);
php.writeFile(`${documentRoot}/index.php`, `<?php echo 'Hello wp-now!';`);

output?.log(`directory: ${options.projectPath}`);
output?.log(`mode: ${options.mode}`);
output?.log(`php: ${options.phpVersion}`);
output?.log(`wp: ${options.wordPressVersion}`);
if (options.mode === WPNowMode.INDEX) {
await runIndexMode(php, options);
return { php, options };
await applyToInstances(phpInstances, async (_php) => {
runIndexMode(_php, options);
});
return { php, phpInstances, options };
}
await downloadWordPress(options.wordPressVersion);
await downloadSqliteIntegrationPlugin();
await downloadMuPlugins();
const isFirstTimeProject = !fs.existsSync(options.wpContentPath);
switch (options.mode) {
case WPNowMode.WP_CONTENT:
await runWpContentMode(php, options);
break;
case WPNowMode.WORDPRESS_DEVELOP:
await runWordPressDevelopMode(php, options);
break;
case WPNowMode.WORDPRESS:
await runWordPressMode(php, options);
break;
case WPNowMode.PLUGIN:
await runPluginOrThemeMode(php, options);
break;
case WPNowMode.THEME:
await runPluginOrThemeMode(php, options);
break;
}
await applyToInstances(phpInstances, async (_php) => {
switch (options.mode) {
case WPNowMode.WP_CONTENT:
await runWpContentMode(_php, options);
break;
case WPNowMode.WORDPRESS_DEVELOP:
await runWordPressDevelopMode(_php, options);
break;
case WPNowMode.WORDPRESS:
await runWordPressMode(_php, options);
break;
case WPNowMode.PLUGIN:
await runPluginOrThemeMode(_php, options);
break;
case WPNowMode.THEME:
await runPluginOrThemeMode(_php, options);
break;
}
});

await installationStep2(php);
await login(php, {
username: 'admin',
Expand All @@ -102,6 +127,7 @@ export default async function startWPNow(

return {
php,
phpInstances,
options,
};
}
Expand Down

0 comments on commit a8ede03

Please sign in to comment.