<?php
namespace Illuminate\Foundation\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Events\VendorTagPublished;
use Illuminate\Support\Arr;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use League\Flysystem\Filesystem as Flysystem;
use League\Flysystem\Local\LocalFilesystemAdapter as LocalAdapter;
use League\Flysystem\MountManager;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\Visibility;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'vendor:publish')]
class VendorPublishCommand extends Command
{
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* The provider to publish.
*
* @var string
*/
protected $provider = null;
/**
* The tags to publish.
*
* @var array
*/
protected $tags = [];
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'vendor:publish
{--existing : Publish and overwrite only the files that have already been published}
{--force : Overwrite any existing files}
{--all : Publish assets for all service providers without prompt}
{--provider= : The service provider that has assets you want to publish}
{--tag=* : One or many tags that have assets you want to publish}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'vendor:publish';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Publish any publishable assets from vendor packages';
/**
* Create a new command instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->determineWhatShouldBePublished();
foreach ($this->tags ?: [null] as $tag) {
$this->publishTag($tag);
}
}
/**
* Determine the provider or tag(s) to publish.
*
* @return void
*/
protected function determineWhatShouldBePublished()
{
if ($this->option('all')) {
return;
}
[$this->provider, $this->tags] = [
$this->option('provider'), (array) $this->option('tag'),
];
if (! $this->provider && ! $this->tags) {
$this->promptForProviderOrTag();
}
}
/**
* Prompt for which provider or tag to publish.
*
* @return void
*/
protected function promptForProviderOrTag()
{
$choice = $this->components->choice(
"Which provider 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(
['<comment>Publish files from all providers and tags listed below</comment>'],
preg_filter('/^/', '<fg=gray>Provider:</> ', Arr::sort(ServiceProvider::publishableProviders())),
preg_filter('/^/', '<fg=gray>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)
{
$published = false;
$pathsToPublish = $this->pathsToPublish($tag);
if ($publishing = count($pathsToPublish) > 0) {
$this->components->info(sprintf(
'Publishing %sassets',
$tag ? "[$tag] " : '',
));
}
foreach ($pathsToPublish as $from => $to) {
$this->publishItem($from, $to);
}
if ($publishing === false) {
$this->components->info('No publishable resources for tag ['.$tag.'].');
} else {
$this->laravel['events']->dispatch(new VendorTagPublished($tag, $pathsToPublish));
$this->newLine();
}
}
/**
* 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->components->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->option('existing') && (! $this->files->exists($to) || $this->option('force')))
|| ($this->option('existing') && $this->files->exists($to))) {
$this->createParentDirectory(dirname($to));
$this->files->copy($from, $to);
$this->status($from, $to, 'file');
} else {
if ($this->option('existing')) {
$this->components->twoColumnDetail(sprintf(
'File [%s] does not exist',
str_replace(base_path().'/', '', $to),
), '<fg=yellow;options=bold>SKIPPED</>');
} else {
$this->components->twoColumnDetail(sprintf(
'File [%s] already exists',
str_replace(base_path().'/', '', realpath($to)),
), '<fg=yellow;options=bold>SKIPPED</>');
}
}
}
/**
* Publish the directory to the given directory.
*
* @param string $from
* @param string $to
* @return void
*/
protected function publishDirectory($from, $to)
{
$visibility = PortableVisibilityConverter::fromArray([], Visibility::PUBLIC);
$this->moveManagedFiles(new MountManager([
'from' => new Flysystem(new LocalAdapter($from)),
'to' => new Flysystem(new LocalAdapter($to, $visibility)),
]));
$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) {
$path = Str::after($file['path'], 'from://');
if (
$file['type'] === 'file'
&& (
(! $this->option('existing') && (! $manager->fileExists('to://'.$path) || $this->option('force')))
|| ($this->option('existing') && $manager->fileExists('to://'.$path))
)
) {
$manager->write('to://'.$path, $manager->read($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->components->task(sprintf(
'Copying %s [%s] to [%s]',
$type,
$from,
$to,
));
}
}