diff --git a/src/Acorn/Bootstrap/WPCLI.php b/src/Acorn/Bootstrap/WPCLI.php new file mode 100644 index 00000000..21bebd1a --- /dev/null +++ b/src/Acorn/Bootstrap/WPCLI.php @@ -0,0 +1,46 @@ +app = $app; + if ($app->runningInConsole() && \defined('WP_CLI') && WP_CLI) { + $this->registerCommands(); + } + } + + protected function registerCommands() + { + \WP_CLI::add_command( + 'acorn view:cache', + $this->app->make(\Roots\Acorn\Console\ViewCacheCommand::class) + ); + + \WP_CLI::add_command( + 'acorn view:clear', + $this->app->make(\Roots\Acorn\Console\ViewClearCommand::class) + ); + + \WP_CLI::add_command( + 'acorn vendor:publish', + $this->app->make(\Roots\Acorn\Console\VendorPublishCommand::class) + ); + + \WP_CLI::add_command( + 'acorn make:provider', + $this->app->make(\Roots\Acorn\Console\ProviderMakeCommand::class) + ); + + \WP_CLI::add_command( + 'acorn make:composer', + $this->app->make(\Roots\Acorn\Console\ComposerMakeCommand::class) + ); + } +} diff --git a/src/Acorn/Console/Command.php b/src/Acorn/Console/Command.php new file mode 100644 index 00000000..cf91682c --- /dev/null +++ b/src/Acorn/Console/Command.php @@ -0,0 +1,188 @@ +files = $files; + $this->app = $app; + } + + /** + * Parse associated args from WP-CLI + * + * @param array $assoc_args + */ + public function parse(array $assoc_args) + { + foreach ($assoc_args as $key => $value) { + if (! property_exists($this, $key)) { + continue; + } + $this->{$key} = is_array($this->{$key}) + ? explode(',', $value) + : $value; + } + } + + /** + * Entry point for WP-CLI. + * + * Consider using PHPDoc to annotate this method when implemented. The + * annotations should describe the command and provide examples. + * + * @link https://make.wordpress.org/cli/handbook/commands-cookbook/#annotating-with-phpdoc + * + * @param array $args + * @param array $assoc_args + */ + abstract public function __invoke($args, $assoc_args); + + /** + * Run a WP-CLI Acorn command + * + * @param string $command WP-CLI Acorn command to run, including arguments. + * @param array $options Configuration options for command execution + * + * @return mixed + */ + protected function call($command, $options = []) + { + return \WP_CLI::runcommand("acorn {$command}", $options); + } + + /** + * Display success message prefixed with "Success: ". + * + * Success message is written to STDOUT. + * + * Typically recommended to inform user of successful script conclusion. + * + * @param string $message Message to write to STDOUT. + * + * @return void + */ + protected function success($message) + { + \WP_CLI::success($message); + } + + /** + * Display warning message prefixed with "Warning: ". + * + * Warning message is written to STDERR. + * + * @param string $message Message to write to STDOUT. + * + * @return void + */ + protected function warning($message) + { + \WP_CLI::warning($message); + } + + /** + * Display error message prefixed with "Error: " and exit script. + * + * Error message is written to STDERR. + * + * @param string|array $message Message to write to STDOUT. + * @param bool|int $exit Exit code for application + * + * @return void + */ + protected function error($message, $exit = true) + { + \WP_CLI::{is_array($message) ? 'error_multi_line' : 'error'}($message); + } + + /** + * Display informational message without prefix, and ignore `--quiet`. + * + * Message is written to STDOUT. + * + * @param string $message Message to write to STDOUT. + * + * @return void + */ + protected function line($message) + { + \WP_CLI::line($message); + } + + /** + * Display informational message without prefix. + * + * Message is written to STDOUT, or discarded when `--quiet` flag is supplied. + * + * @param string $message Message to write to STDOUT. + * + * @return void + */ + protected function info($message) + { + \WP_CLI::log($message); + } + + /** + * Display debug message prefixed with "Debug: " when `--debug` is used. + * + * Debug message is written to STDERR, and includes script execution time. + * + * @param string $message Message to write to STDOUT. + * @param string|bool $group Organize debug message to a specific group. + * + * @return void + */ + protected function debug($message, $group = false) + { + \WP_CLI::debug($message, $group); + } + + /** + * Display success message prefixed with "Success: ". + * + * @param string $message Message to write to STDOUT. + * + * @return void + */ + protected function choice($message, iterable $choices) + { + fwrite(STDOUT, "$message\n\n "); + foreach ($choices as $key => $value) { + fwrite(STDOUT, "[{$key}] - {$value}\n "); + } + fwrite(STDOUT, "\n >"); + + $answer = trim(fgets(STDIN)); + + if (! isset($choices[$answer])) { + $this->info("{$answer} is not a valid option."); + return $this->choice($message, $choices); + } + + return $choices[$answer]; + } +} diff --git a/src/Acorn/Console/ComposerMakeCommand.php b/src/Acorn/Console/ComposerMakeCommand.php new file mode 100644 index 00000000..d61ca483 --- /dev/null +++ b/src/Acorn/Console/ComposerMakeCommand.php @@ -0,0 +1,56 @@ + + * : The name of the composer. + * + * [--force] + * : Overwrite any existing files + * + * ## EXAMPLES + * + * wp acorn make:composer + */ + public function __invoke($args, $assoc_args) + { + list($name) = $args; + $this->parse($assoc_args + compact('name')); + $this->handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__ . '/stubs/composer.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . '\Composers'; + } +} diff --git a/src/Acorn/Console/GeneratorCommand.php b/src/Acorn/Console/GeneratorCommand.php new file mode 100644 index 00000000..e6043959 --- /dev/null +++ b/src/Acorn/Console/GeneratorCommand.php @@ -0,0 +1,221 @@ +qualifyClass($this->getNameInput()); + + $path = $this->getPath($name); + + // First we will check to see if the class already exists. If it does, we don't want + // to create the class and overwrite the user's code. So, we will bail out so the + // code is untouched. Otherwise, we will continue generating this class' files. + if ((! $this->force) && $this->alreadyExists($this->getNameInput())) { + $this->error($this->type . ' already exists!'); + + return false; + } + + // Next, we will generate the path to the location where this class' file should get + // written. Then, we will build the class and make the proper replacements on the + // stub files so that it gets the correctly formatted namespace and class name. + $this->makeDirectory($path); + + $this->files->put($path, $this->buildClass($name)); + + $this->success($this->type . ' created successfully.'); + } + + /** + * Parse the class name and format according to the root namespace. + * + * @param string $name + * @return string + */ + protected function qualifyClass($name) + { + $name = ltrim($name, '\\/'); + + $rootNamespace = $this->rootNamespace(); + + if (Str::startsWith($name, $rootNamespace)) { + return $name; + } + + $name = str_replace('/', '\\', $name); + + return $this->qualifyClass( + $this->getDefaultNamespace(trim($rootNamespace, '\\')) . '\\' . $name + ); + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace; + } + + /** + * Determine if the class already exists. + * + * @param string $rawName + * @return bool + */ + protected function alreadyExists($rawName) + { + return $this->files->exists($this->getPath($this->qualifyClass($rawName))); + } + + /** + * Get the destination class path. + * + * @param string $name + * @return string + */ + protected function getPath($name) + { + $name = Str::replaceFirst($this->rootNamespace(), '', $name); + + return $this->app->path() . '/' . str_replace('\\', '/', $name) . '.php'; + } + + /** + * Build the directory for the class if necessary. + * + * @param string $path + * @return string + */ + protected function makeDirectory($path) + { + if (! $this->files->isDirectory(dirname($path))) { + $this->files->makeDirectory(dirname($path), 0777, true, true); + } + + return $path; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function buildClass($name) + { + $stub = $this->files->get($this->getStub()); + + return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); + } + + /** + * Replace the namespace for the given stub. + * + * @param string $stub + * @param string $name + * @return $this + */ + protected function replaceNamespace(&$stub, $name) + { + $stub = str_replace( + ['DummyNamespace', 'DummyRootNamespace'], + [$this->getNamespace($name), $this->rootNamespace()], + $stub + ); + + return $this; + } + + /** + * Get the full namespace for a given class, without the class name. + * + * @param string $name + * @return string + */ + protected function getNamespace($name) + { + return trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + } + + /** + * Replace the class name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceClass($stub, $name) + { + $class = str_replace($this->getNamespace($name) . '\\', '', $name); + + return str_replace('DummyClass', $class, $stub); + } + + /** + * Get the desired class name from the input. + * + * @return string + */ + protected function getNameInput() + { + return trim($this->name); + } + + /** + * Get the root namespace for the class. + * + * @return string + */ + protected function rootNamespace() + { + return $this->app->getNamespace(); + } + + /** + * Get the model for the default guard's user provider. + * + * @return string|null + */ + protected function userProviderModel() + { + $guard = config('auth.defaults.guard'); + + $provider = config("auth.guards.{$guard}.provider"); + + return config("auth.providers.{$provider}.model"); + } +} diff --git a/src/Acorn/Console/ProviderMakeCommand.php b/src/Acorn/Console/ProviderMakeCommand.php new file mode 100644 index 00000000..f501bcf7 --- /dev/null +++ b/src/Acorn/Console/ProviderMakeCommand.php @@ -0,0 +1,56 @@ + + * : The name of the provider. + * + * [--force] + * : Overwrite any existing files + * + * ## EXAMPLES + * + * wp acorn make:provider + */ + public function __invoke($args, $assoc_args) + { + list($name) = $args; + $this->parse($assoc_args + compact('name')); + $this->handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__ . '/stubs/provider.stub'; + } + + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . '\Providers'; + } +} diff --git a/src/Acorn/Console/VendorPublishCommand.php b/src/Acorn/Console/VendorPublishCommand.php new file mode 100644 index 00000000..2ee8ae8b --- /dev/null +++ b/src/Acorn/Console/VendorPublishCommand.php @@ -0,0 +1,239 @@ +] + * : The service provider that has assets you want to publish + * + * [--tags=] + * : One or many (comma separated) tags that have assets you want to publish + * + * ## EXAMPLES + * + * wp acorn vendor:publish + */ + public function __invoke($args, $assoc_args) + { + $this->parse($assoc_args); + + $this->determineWhatShouldBePublished(); + + foreach ($this->tags ?: [null] as $tag) { + $this->publishTag($tag); + } + + $this->success('Publishing complete.'); + } + + /** + * Determine the provider or tag(s) to publish. + * + * @return void + */ + protected function determineWhatShouldBePublished() + { + if ($this->all) { + return; + } + + if (! $this->provider && ! $this->tags) { + $this->promptForProviderOrTag(); + } + } + + /** + * Prompt for which provider or tag to publish. + * + * @return void + */ + protected function promptForProviderOrTag() + { + $choice = $this->choice( + "Which provider's or tag's files would you like to publish?", + $choices = $this->publishableChoices() + ); + + if ($choice == $choices[0] || is_null($choice)) { + return; + } + + $this->parseChoice($choice); + } + + /** + * The choices available via the prompt. + * + * @return array + */ + protected function publishableChoices() + { + return array_merge( + ['Publish files from all providers and tags listed below'], + preg_filter('/^/', 'Provider: ', Arr::sort(ServiceProvider::publishableProviders())), + preg_filter('/^/', 'Tag: ', Arr::sort(ServiceProvider::publishableGroups())) + ); + } + + /** + * Parse the answer that was given via the prompt. + * + * @param string $choice + * @return void + */ + protected function parseChoice($choice) + { + [$type, $value] = explode(': ', strip_tags($choice)); + if ($type === 'Provider') { + $this->provider = $value; + } elseif ($type === 'Tag') { + $this->tags = [$value]; + } + } + + /** + * Publishes the assets for a tag. + * + * @param string $tag + * @return mixed + */ + protected function publishTag($tag) + { + foreach ($this->pathsToPublish($tag) as $from => $to) { + $this->publishItem($from, $to); + } + } + + /** + * Get all of the paths to publish. + * + * @param string $tag + * @return array + */ + protected function pathsToPublish($tag) + { + return ServiceProvider::pathsToPublish($this->provider, $tag); + } + + /** + * Publish the given item from and to the given location. + * + * @param string $from + * @param string $to + * @return void + */ + protected function publishItem($from, $to) + { + if ($this->files->isFile($from)) { + return $this->publishFile($from, $to); + } elseif ($this->files->isDirectory($from)) { + return $this->publishDirectory($from, $to); + } + $this->error("Can't locate path: <{$from}>"); + } + + /** + * Publish the file to the given path. + * + * @param string $from + * @param string $to + * @return void + */ + protected function publishFile($from, $to) + { + if (! $this->files->exists($to) || $this->force) { + $this->createParentDirectory(dirname($to)); + $this->files->copy($from, $to); + $this->status($from, $to, 'File'); + } + } + + /** + * Publish the directory to the given directory. + * + * @param string $from + * @param string $to + * @return void + */ + protected function publishDirectory($from, $to) + { + $this->moveManagedFiles(new MountManager([ + 'from' => new Flysystem(new LocalAdapter($from)), + 'to' => new Flysystem(new LocalAdapter($to)), + ])); + $this->status($from, $to, 'Directory'); + } + + /** + * Move all the files in the given MountManager. + * + * @param \League\Flysystem\MountManager $manager + * @return void + */ + protected function moveManagedFiles($manager) + { + foreach ($manager->listContents('from://', true) as $file) { + if ($file['type'] === 'file' && (! $manager->has('to://' . $file['path']) || $this->option('force'))) { + $manager->put('to://' . $file['path'], $manager->read('from://' . $file['path'])); + } + } + } + + /** + * Create the directory to house the published files if needed. + * + * @param string $directory + * @return void + */ + protected function createParentDirectory($directory) + { + if (! $this->files->isDirectory($directory)) { + $this->files->makeDirectory($directory, 0755, true); + } + } + + /** + * Write a status message to the console. + * + * @param string $from + * @param string $to + * @param string $type + * @return void + */ + protected function status($from, $to, $type) + { + $from = str_replace(base_path(), '', realpath($from)); + $to = str_replace(base_path(), '', realpath($to)); + $this->info('Copied ' . $type . ' [' . $from . '] To [' . $to . ']'); + } +} diff --git a/src/Acorn/Console/ViewCacheCommand.php b/src/Acorn/Console/ViewCacheCommand.php new file mode 100644 index 00000000..79e5020a --- /dev/null +++ b/src/Acorn/Console/ViewCacheCommand.php @@ -0,0 +1,72 @@ +call('view:clear'); + + $this->paths()->each(function ($path) { + $this->compileViews($this->bladeFilesIn([$path])); + }); + + $this->success('Blade templates cached successfully!'); + } + + /** + * Compile the given view files. + * + * @param \Illuminate\Support\Collection $views + * @return void + */ + protected function compileViews(Collection $views) + { + $compiler = $this->app['view']->getEngineResolver()->resolve('blade')->getCompiler(); + + $views->map(function (SplFileInfo $file) use ($compiler) { + $compiler->compile($file->getRealPath()); + }); + } + + /** + * Get the Blade files in the given path. + * + * @param array $paths + * @return \Illuminate\Support\Collection + */ + protected function bladeFilesIn(array $paths) + { + return collect( + Finder::create() + ->in($paths) + ->exclude('vendor') + ->name('*.blade.php') + ->files() + ); + } + + /** + * Get all of the possible view paths. + * + * @return \Illuminate\Support\Collection + */ + protected function paths() + { + $finder = $this->app['view.finder']; + + return collect($finder->getPaths())->merge( + collect($finder->getHints())->flatten() + ); + } +} diff --git a/src/Acorn/Console/ViewClearCommand.php b/src/Acorn/Console/ViewClearCommand.php new file mode 100644 index 00000000..1aa62e20 --- /dev/null +++ b/src/Acorn/Console/ViewClearCommand.php @@ -0,0 +1,24 @@ +app['config']['view.compiled']; + + if (! $path) { + $this->error('View path not found.'); + } + + $this->files->delete($this->files->glob("{$path}/*")); + $this->success('Compiled views cleared!'); + } +} diff --git a/src/Acorn/Console/stubs/composer.stub b/src/Acorn/Console/stubs/composer.stub new file mode 100644 index 00000000..d9d57b27 --- /dev/null +++ b/src/Acorn/Console/stubs/composer.stub @@ -0,0 +1,31 @@ +