diff --git a/README.md b/README.md index cb9c8f77..31987b3d 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,12 @@ The Blueprints library is distributed as a .phar library. To build the .phar fil vendor/bin/box compile ``` +Note that in box.json, the `"check-requirements"` option is set to `false`. Somehow, keeping it as `true` results in a +.phar file +that breaks HTTP requests in Playground. @TODO: Investigate why this is the case. + To try the built .phar file, run: ```shell rm -rf new-wp/* && USE_PHAR=1 php blueprint_compiling.php ``` - diff --git a/blueprint_compiling.php b/blueprint_compiling.php index 6e01385e..0fa527ab 100644 --- a/blueprint_compiling.php +++ b/blueprint_compiling.php @@ -1,5 +1,6 @@ true, ] ) ->withPlugins( [ + // Required for withContent(): + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', 'https://downloads.wordpress.org/plugin/hello-dolly.zip', 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', ] ) @@ -37,6 +40,6 @@ ->toBlueprint(); -$results = run_blueprint( $blueprint, __DIR__ . '/new-wp' ); +$results = run_blueprint( $blueprint, ContainerBuilder::ENVIRONMENT_NATIVE, __DIR__ . '/new-wp' ); var_dump( $results ); diff --git a/src/WordPress/Blueprints/ContainerBuilder.php b/src/WordPress/Blueprints/ContainerBuilder.php index f468466d..ae88c255 100644 --- a/src/WordPress/Blueprints/ContainerBuilder.php +++ b/src/WordPress/Blueprints/ContainerBuilder.php @@ -59,21 +59,22 @@ use WordPress\Blueprints\Runner\Step\UnzipStepRunner; use WordPress\Blueprints\Runner\Step\WPCLIStepRunner; use WordPress\Blueprints\Runner\Step\WriteFileStepRunner; -use WordPress\Blueprints\Runtime\NativePHPRuntime; +use WordPress\Blueprints\Runtime\Runtime; use WordPress\Blueprints\Runtime\RuntimeInterface; use WordPress\DataSource\FileSource; +use WordPress\DataSource\PlaygroundFetchSource; use WordPress\DataSource\ProgressEvent; use WordPress\DataSource\UrlSource; class ContainerBuilder { - const RUNTIME_NATIVE = 'native'; - const RUNTIME_PLAYGROUND = 'playground'; - const RUNTIME_WP_NOW = 'wp-now'; - const RUNTIMES = [ - self::RUNTIME_NATIVE, - self::RUNTIME_PLAYGROUND, - self::RUNTIME_WP_NOW, + const ENVIRONMENT_NATIVE = 'native'; + const ENVIRONMENT_PLAYGROUND = 'playground'; + const ENVIRONMENT_WP_NOW = 'wp-now'; + const ENVIRONMENTS = [ + self::ENVIRONMENT_NATIVE, + self::ENVIRONMENT_PLAYGROUND, + self::ENVIRONMENT_WP_NOW, ]; protected $container; @@ -83,13 +84,13 @@ public function __construct() { } - public function build( RuntimeInterface $runtime ) { + public function build( string $environment, RuntimeInterface $runtime ) { $container = $this->container; $container['runtime'] = function () use ( $runtime ) { return $runtime; }; - if ( $runtime instanceof NativePHPRuntime ) { + if ( $environment === static::ENVIRONMENT_NATIVE ) { $container['downloads_cache'] = function ( $c ) { return new FileCache(); }; @@ -101,6 +102,18 @@ public function build( RuntimeInterface $runtime ) { echo $event->url . ' ' . $event->downloadedBytes . '/' . $event->totalBytes . " \r"; }; }; + $container[ "resource.resolver." . UrlResource::DISCRIMINATOR ] = function ( $c ) { + return new UrlResourceResolver( $c['data_source.url'] ); + }; + } elseif ( $environment === static::ENVIRONMENT_PLAYGROUND ) { + $container[ "resource.resolver." . UrlResource::DISCRIMINATOR ] = function ( $c ) { + return new UrlResourceResolver( $c['data_source.playground_fetch'] ); + }; + $container['progress_reporter'] = function ( $c ) { + return function ( ProgressEvent $event ) { + echo $event->url . ' ' . $event->downloadedBytes . '/' . $event->totalBytes . " \r"; + }; + }; } else { throw new InvalidArgumentException( "Not implemented yet" ); } @@ -218,9 +231,6 @@ function () use ( $c ) { return new RunSQLStepRunner(); }; - $container[ "resource.resolver." . UrlResource::DISCRIMINATOR ] = function ( $c ) { - return new UrlResourceResolver( $c['data_source.url'] ); - }; $container[ "resource.resolver." . FilesystemResource::DISCRIMINATOR ] = function () { return new FilesystemResourceResolver(); }; @@ -262,6 +272,9 @@ function () use ( $c ) { $container['data_source.url'] = function ( $c ) { return new UrlSource( $c['http_client'], $c['downloads_cache'] ); }; + $container['data_source.playground_fetch'] = function ( $c ) { + return new PlaygroundFetchSource(); + }; // Add a progress listener to all data sources foreach ( $container->keys() as $key ) { diff --git a/src/WordPress/Blueprints/Model/BlueprintBuilder.php b/src/WordPress/Blueprints/Model/BlueprintBuilder.php index d7260d1b..0253277d 100644 --- a/src/WordPress/Blueprints/Model/BlueprintBuilder.php +++ b/src/WordPress/Blueprints/Model/BlueprintBuilder.php @@ -87,7 +87,7 @@ public function withContent( $wxrs ) { if ( ! is_array( $wxrs ) ) { $wxrs = [ $wxrs ]; } - $this->withPlugin( 'https://downloads.wordpress.org/plugin/wordpress-importer.zip' ); + // @TODO: Should this automatically add the importer plugin if it's not already installed? foreach ( $wxrs as $wxr ) { $this->addStep( ( new ImportFileStep() ) diff --git a/src/WordPress/Blueprints/Runner/Step/SetSiteOptionsStepRunner.php b/src/WordPress/Blueprints/Runner/Step/SetSiteOptionsStepRunner.php index 534a141c..ce8d51b3 100644 --- a/src/WordPress/Blueprints/Runner/Step/SetSiteOptionsStepRunner.php +++ b/src/WordPress/Blueprints/Runner/Step/SetSiteOptionsStepRunner.php @@ -17,7 +17,7 @@ function run( SetSiteOptionsStep $input, Tracker $tracker ) { // with a separate wp-cli command. return $this->getRuntime()->evalPhpInSubProcess( <<<'CODE' $value) { update_option($name, $value); diff --git a/src/WordPress/Blueprints/Runtime/NativePHPRuntime.php b/src/WordPress/Blueprints/Runtime/Runtime.php similarity index 95% rename from src/WordPress/Blueprints/Runtime/NativePHPRuntime.php rename to src/WordPress/Blueprints/Runtime/Runtime.php index ec8b807d..79240a11 100644 --- a/src/WordPress/Blueprints/Runtime/NativePHPRuntime.php +++ b/src/WordPress/Blueprints/Runtime/Runtime.php @@ -7,7 +7,7 @@ use Symfony\Component\Process\Process; use function WordPress\Blueprints\join_paths; -class NativePHPRuntime implements RuntimeInterface { +class Runtime implements RuntimeInterface { public Filesystem $fs; @@ -33,7 +33,7 @@ public function getDocumentRoot(): string { } public function resolvePath( string $path ): string { - return Path::makeAbsolute($path, $this->getDocumentRoot()); + return Path::makeAbsolute( $path, $this->getDocumentRoot() ); } public function withTemporaryDirectory( $callback ) { diff --git a/src/WordPress/Blueprints/functions.php b/src/WordPress/Blueprints/functions.php index ae7281ce..9ccbb921 100644 --- a/src/WordPress/Blueprints/functions.php +++ b/src/WordPress/Blueprints/functions.php @@ -3,13 +3,12 @@ namespace WordPress\Blueprints; use Symfony\Component\Filesystem\Exception\IOException; -use WordPress\Blueprints\Runtime\NativePHPRuntime; +use WordPress\Blueprints\Runtime\Runtime; -function run_blueprint( $json, $documentRoot = '/wordpress' ) { +function run_blueprint( $json, $environment, $documentRoot = '/wordpress' ) { $c = ( new ContainerBuilder() )->build( - new NativePHPRuntime( - $documentRoot - ) + $environment, + new Runtime( $documentRoot ) ); return $c['blueprint.engine']->runBlueprint( $json ); diff --git a/src/WordPress/DataSource/PlaygroundFetchSource.php b/src/WordPress/DataSource/PlaygroundFetchSource.php new file mode 100644 index 00000000..569cf5d9 --- /dev/null +++ b/src/WordPress/DataSource/PlaygroundFetchSource.php @@ -0,0 +1,40 @@ + [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ], + $pipes + ); + // This prevents the process handle from getting garbage collected and + // breaking the stdout pipe. However, how the program never terminates. + // Presumably we need to peek() on the resource handle and close the + // process handle when it's done. + // Without this line, we get the following error: + // PHP Fatal error: Uncaught TypeError: stream_copy_to_stream(): supplied resource is not a valid stream resource i + // var_dump()–ing first says + // resource(457) of type (stream) + // but then it says + // resource(457) of type (Unknown) + $this->proc_handles[] = $proc_handle; + + return $pipes[1]; + } + +} + diff --git a/src/WordPress/DataSource/UrlSource.php b/src/WordPress/DataSource/UrlSource.php index 213d9e8c..fc445636 100644 --- a/src/WordPress/DataSource/UrlSource.php +++ b/src/WordPress/DataSource/UrlSource.php @@ -37,7 +37,7 @@ public function stream( $resourceIdentifier ) { if ( $this->cache->has( $url ) ) { // Return a stream resource. // @TODO: Stream directly from the cache - $cached = $this->cache->get( $url ); + $cached = $this->cache->get( $url ); $data_size = strlen( $cached ); $this->events->dispatch( new ProgressEvent( $url, @@ -60,17 +60,17 @@ public function stream( $resourceIdentifier ) { ) ); }, ] ); - $stream = StreamWrapper::createResource( $response, $this->client ); + $stream = StreamWrapper::createResource( $response, $this->client ); if ( ! $stream ) { throw new \Exception( 'Failed to download file' ); } $onChunk = function ( $chunk ) use ( $url, $response, $stream ) { // Handle response caching - // @TODO: don't buffer, just keep appending to the cache. static $bufferedChunks = []; $bufferedChunks[] = $chunk; if ( feof( $stream ) ) { $this->cache->set( $url, implode( '', $bufferedChunks ) ); + $bufferedChunks = []; } }; $onClose = function () use ( $response ) { diff --git a/src/WordPress/Zip/ZipStreamReader.php b/src/WordPress/Zip/ZipStreamReader.php index a98898e5..409e31f5 100644 --- a/src/WordPress/Zip/ZipStreamReader.php +++ b/src/WordPress/Zip/ZipStreamReader.php @@ -57,10 +57,10 @@ static public function readEntry( $fp ) { * @param resource $stream */ static protected function readFileEntry( $stream ): ZipFileEntry { - $data = self::read_bytes( $stream, 26 ); - $data = unpack( 'vversionNeeded/vgeneralPurpose/vcompressionMethod/vlastModifiedTime/vlastModifiedDate/Vcrc/VcompressedSize/VuncompressedSize/vpathLength/vextraLength', + $data = self::read_bytes( $stream, 26 ); + $data = unpack( 'vversionNeeded/vgeneralPurpose/vcompressionMethod/vlastModifiedTime/vlastModifiedDate/Vcrc/VcompressedSize/VuncompressedSize/vpathLength/vextraLength', $data ); - $path = self::read_bytes( $stream, $data['pathLength'] ); + $path = self::read_bytes( $stream, $data['pathLength'] ); $extra = self::read_bytes( $stream, $data['extraLength'] ); $bytes = self::read_bytes( $stream, $data['compressedSize'] ); @@ -119,11 +119,11 @@ static protected function readFileEntry( $stream ): ZipFileEntry { * @param resource stream */ static protected function readCentralDirectoryEntry( $stream ): ZipCentralDirectoryEntry { - $data = static::read_bytes( $stream, 42 ); - $data = unpack( 'vversionCreated/vversionNeeded/vgeneralPurpose/vcompressionMethod/vlastModifiedTime/vlastModifiedDate/Vcrc/VcompressedSize/VuncompressedSize/vpathLength/vextraLength/vfileCommentLength/vdiskNumber/vinternalAttributes/VexternalAttributes/VfirstByteAt', + $data = static::read_bytes( $stream, 42 ); + $data = unpack( 'vversionCreated/vversionNeeded/vgeneralPurpose/vcompressionMethod/vlastModifiedTime/vlastModifiedDate/Vcrc/VcompressedSize/VuncompressedSize/vpathLength/vextraLength/vfileCommentLength/vdiskNumber/vinternalAttributes/VexternalAttributes/VfirstByteAt', $data ); - $path = static::read_bytes( $stream, $data['pathLength'] ); - $extra = static::read_bytes( $stream, $data['extraLength'] ); + $path = static::read_bytes( $stream, $data['pathLength'] ); + $extra = static::read_bytes( $stream, $data['extraLength'] ); $fileComment = static::read_bytes( $stream, $data['fileCommentLength'] ); return new ZipCentralDirectoryEntry( @@ -204,7 +204,7 @@ static protected function read_bytes( $stream, $length ): string|bool { return false; } $length -= strlen( $chunk ); - $data .= $chunk; + $data .= $chunk; if ( $length === 0 ) { break; diff --git a/src/WordPress/Zip/functions.php b/src/WordPress/Zip/functions.php index e836ba3d..3c009533 100644 --- a/src/WordPress/Zip/functions.php +++ b/src/WordPress/Zip/functions.php @@ -11,7 +11,7 @@ function zip_extract_to( $fp, $toPath ) { if ( ! $entry->isFileEntry() ) { continue; } - $path = $toPath . '/' . sanitize_path( $entry->path ); + $path = $toPath . '/' . sanitize_path( $entry->path ); $parent = dirname( $path ); if ( ! is_dir( $parent ) ) { mkdir( $parent, 0777, true ); diff --git a/tests/Unit/RmStepTest.php b/tests/Unit/RmStepTest.php index 659cba0d..231935d1 100644 --- a/tests/Unit/RmStepTest.php +++ b/tests/Unit/RmStepTest.php @@ -5,112 +5,115 @@ use WordPress\Blueprints\BlueprintException; use WordPress\Blueprints\Model\DataClass\RmStep; use WordPress\Blueprints\Runner\Step\RmStepRunner; -use WordPress\Blueprints\Runtime\NativePHPRuntime; +use WordPress\Blueprints\Runtime\Runtime; -beforeEach(function() { - $this->documentRoot = Path::makeAbsolute("test", sys_get_temp_dir()); - $this->runtime = new NativePHPRuntime($this->documentRoot); +beforeEach( function () { + $this->documentRoot = Path::makeAbsolute( "test", sys_get_temp_dir() ); + $this->runtime = new Runtime( $this->documentRoot ); - $this->step = new RmStepRunner(); - $this->step->setRuntime($this->runtime); + $this->step = new RmStepRunner(); + $this->step->setRuntime( $this->runtime ); - $this->fileSystem = new Filesystem(); -}); + $this->fileSystem = new Filesystem(); +} ); -afterEach(function() { - $this->fileSystem->remove($this->documentRoot); -}); +afterEach( function () { + $this->fileSystem->remove( $this->documentRoot ); +} ); -it('should remove a directory (using an absolute path)', function() { - $absolutePath = $this->runtime->resolvePath("dir"); - $this->fileSystem->mkdir($absolutePath); +it( 'should remove a directory (using an absolute path)', function () { + $absolutePath = $this->runtime->resolvePath( "dir" ); + $this->fileSystem->mkdir( $absolutePath ); - $input = new RmStep(); - $input->path = $absolutePath; + $input = new RmStep(); + $input->path = $absolutePath; - $this->step->run($input); + $this->step->run( $input ); - expect($this->fileSystem->exists($absolutePath))->toBeFalse(); -}); + expect( $this->fileSystem->exists( $absolutePath ) )->toBeFalse(); +} ); -it('should remove a directory (using a relative path)', function () { - $relativePath = "dir"; - $absolutePath = $this->runtime->resolvePath($relativePath); - $this->fileSystem->mkdir($absolutePath); +it( 'should remove a directory (using a relative path)', function () { + $relativePath = "dir"; + $absolutePath = $this->runtime->resolvePath( $relativePath ); + $this->fileSystem->mkdir( $absolutePath ); - $input = new RmStep(); - $input->path = $relativePath; + $input = new RmStep(); + $input->path = $relativePath; - $this->step->run($input); + $this->step->run( $input ); - expect($this->fileSystem->exists($absolutePath))->toBeFalse(); -}); + expect( $this->fileSystem->exists( $absolutePath ) )->toBeFalse(); +} ); -it ('should remove a directory with a subdirectory', function () { - $relativePath = "dir/subdir"; - $absolutePath = $this->runtime->resolvePath($relativePath); - $this->fileSystem->mkdir($absolutePath); +it( 'should remove a directory with a subdirectory', function () { + $relativePath = "dir/subdir"; + $absolutePath = $this->runtime->resolvePath( $relativePath ); + $this->fileSystem->mkdir( $absolutePath ); - $input = new RmStep(); - $input->path = dirname($relativePath); + $input = new RmStep(); + $input->path = dirname( $relativePath ); - $this->step->run($input); + $this->step->run( $input ); - expect($this->fileSystem->exists(dirname($absolutePath)))->toBeFalse(); -}); + expect( $this->fileSystem->exists( dirname( $absolutePath ) ) )->toBeFalse(); +} ); -it ('should remove a directory with a file', function () { - $relativePath = "dir/file.txt"; - $absolutePath = $this->runtime->resolvePath($relativePath); - $this->fileSystem->dumpFile($absolutePath, "test"); +it( 'should remove a directory with a file', function () { + $relativePath = "dir/file.txt"; + $absolutePath = $this->runtime->resolvePath( $relativePath ); + $this->fileSystem->dumpFile( $absolutePath, "test" ); - $input = new RmStep(); - $input->path = dirname($relativePath); + $input = new RmStep(); + $input->path = dirname( $relativePath ); - $this->step->run($input); + $this->step->run( $input ); - expect($this->fileSystem->exists(dirname($absolutePath)))->toBeFalse(); -}); + expect( $this->fileSystem->exists( dirname( $absolutePath ) ) )->toBeFalse(); +} ); -it ('should remove a file', function () { - $relativePath = "file.txt"; - $absolutePath = $this->runtime->resolvePath($relativePath); - $this->fileSystem->dumpFile($absolutePath, "test"); +it( 'should remove a file', function () { + $relativePath = "file.txt"; + $absolutePath = $this->runtime->resolvePath( $relativePath ); + $this->fileSystem->dumpFile( $absolutePath, "test" ); - $input = new RmStep(); - $input->path = $relativePath; + $input = new RmStep(); + $input->path = $relativePath; - $this->step->run($input); + $this->step->run( $input ); - expect($this->fileSystem->exists($absolutePath))->toBeFalse(); -}); + expect( $this->fileSystem->exists( $absolutePath ) )->toBeFalse(); +} ); -it ('should throw an exception when asked to remove a nonexistent directory (using a relative path)', function() { - $relativePath = "dir"; +it( 'should throw an exception when asked to remove a nonexistent directory (using a relative path)', function () { + $relativePath = "dir"; - $input = new RmStep(); - $input->path = $relativePath; + $input = new RmStep(); + $input->path = $relativePath; - $absolutePath = $this->runtime->resolvePath($relativePath); - expect(fn() => $this->step->run($input))->toThrow(BlueprintException::class, "Failed to remove \"$absolutePath\": the directory or file does not exist."); -}); + $absolutePath = $this->runtime->resolvePath( $relativePath ); + expect( fn() => $this->step->run( $input ) )->toThrow( BlueprintException::class, + "Failed to remove \"$absolutePath\": the directory or file does not exist." ); +} ); -it ('should throw an exception when asked to remove a nonexistent directory (using an absolute path)', function () { - $absolutePath = "/dir"; +it( 'should throw an exception when asked to remove a nonexistent directory (using an absolute path)', function () { + $absolutePath = "/dir"; - $input = new RmStep(); - $input->path = $absolutePath; + $input = new RmStep(); + $input->path = $absolutePath; - expect(fn() => $this->step->run($input))->toThrow(BlueprintException::class, "Failed to remove \"$absolutePath\": the directory or file does not exist."); -}); + expect( fn() => $this->step->run( $input ) )->toThrow( BlueprintException::class, + "Failed to remove \"$absolutePath\": the directory or file does not exist." ); +} ); -it ('should throw an exception when asked to remove a nonexistent file', function() { - $relativePath = "file.txt"; +it( 'should throw an exception when asked to remove a nonexistent file', function () { + $relativePath = "file.txt"; - $input = new RmStep(); - $input->path = $relativePath; + $input = new RmStep(); + $input->path = $relativePath; - $absolutePath = $this->runtime->resolvePath($relativePath); - expect(fn() => $this->step->run($input))->toThrow(BlueprintException::class, "Failed to remove \"$absolutePath\": the directory or file does not exist."); -}); + $absolutePath = $this->runtime->resolvePath( $relativePath ); + expect( fn() => $this->step->run( $input ) )->toThrow( BlueprintException::class, + "Failed to remove \"$absolutePath\": the directory or file does not exist." ); +} );