diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b039a3d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: franzliedke +tidelift: packagist/franzl/studio diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..ad018d9 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Currently, only the [latest stable release](https://github.com/franzliedke/studio/releases) is supported with security updates. + +## Reporting a Vulnerability + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3c564d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: [push, pull_request] + +jobs: + phpspec: + runs-on: ubuntu-latest + strategy: + matrix: + php: [7.2, 7.3, 7.4, 8.0, 8.1] + steps: + - uses: actions/checkout@v1 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Validate Composer files + run: composer validate --no-check-all --strict + - name: Install Composer dependencies + run: composer install --prefer-dist --no-progress + - name: Run tests + run: php vendor/bin/phpspec run + diff --git a/README.md b/README.md index 65c28f4..e973539 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # studio +* [Installation](#installation) +* [Usage](#installation) + * [Workflow](#workflow) + * [Command Reference](#command-reference) +* [License](#license) +* [Contributing](#contributing) +* [For enterprise](#franzlstudio-for-enterprise) + Develop your Composer libraries with style. This package makes it easy to develop Composer packages while using them. @@ -34,9 +42,75 @@ Per project: `composer require --dev franzl/studio` All commands should be run from the root of your project, where the `composer.json` file is located. -### Create a new package skeleton +### Workflow + +Typically, you will want to require one of your own Composer packages in an application. +With Studio, you can pull in your local copy of the package *instead of* the version hosted on Packagist. +The kicker: You can keep developing your library while you dogfood it, but **you won't need to change your composer.json file**. + +#### Loading local packages + +To use one of your own libraries within your application, you need to tell Studio where it can locate the library. +You can do so with the `load` command. +When Composer resolves its dependencies, Studio will then pitch in and symlink your local directory to Composer's `vendor` directory. + +So, to have Studio manage your awesome world domination library, all you have to do is run the following command: + + $ studio load path/to/world-domination + +This command should create a `studio.json` file in the current working directory. +It contains a list of directories for Studio to load. + +Next, if you haven't already done so, make sure you actually require the package in your composer.json: + + "require": { + "my/world-domination": "dev-master" + } + +And finally, tell Studio to set up the symlinks: + + $ composer update my/world-domination + +If all goes well, you should now see a brief message along the following as part of Composer's output: + +> [Studio] Loading path installer + +This is what will happen under the hood: + +1. Composer begins checking dependencies for updates. +2. Studio jumps in and informs Composer to prefer packages from the directories listed in the `studio.json` file over downloading them from Packagist. +3. Composer symlinks these packages into the `vendor` directory or any other appropriate place (e.g. for [custom installers](https://getcomposer.org/doc/articles/custom-installers.md)). + Thus, to your application, these packages will behave just like "normal" Composer packages. +4. Composer generates proper autoloading rules for the Studio packages. +5. For non-Studio packages, Composer works as always. + +**Pro tip:** If you keep all your libraries in one directory, you can let Studio find all of them by using a wildcard: + + $ studio load 'path/to/my/libraries/*' + +#### Kickstarting package development + +If you haven't started world domination yet, Studio also includes a handy generator for new Composer packages. +Besides the usual ceremony, it contains several optional components, such as configuration for unit tests, continuous integration on Travis-CI and others. + +First, we need to create the local directory for the development package: - studio create foo/bar + $ studio create domination + # or if you want to clone a git repo + $ studio create domination --git git@github.com:vendor/domination.git + +After asking you a series of questions, this will create (or download) a package in the `domination` subdirectory inside the current working directory. +There is a good chance that you need a little time to develop this package before publishing it on Packagist. +Therefore, if you ran this command in a Composer application, Studio will offer you to load your new package immediately. +This essentially comes down to running `studio load domination`. + +Finally, don't forget to use `composer require` to actually add your package as a dependency. + +### Command Reference + +#### create: Create a new package skeleton + + $ studio create foo/bar This command creates a skeleton for a new Composer package, already filled with some helpful files to get you started. In the above example, we're creating a new package in the folder `foo/bar` in your project root. @@ -45,26 +119,52 @@ All its dependencies will be available when using Composer. During creation, you will be asked a series of questions to configure your skeleton. This will include things like configuration for testing tools, Travis CI, and autoloading. -### Manage existing packages by cloning a Git repository +#### create --git: Manage existing packages by cloning a Git repository - studio create bar --git git@github.com:me/myrepo.git + $ studio create bar --git git@github.com:me/myrepo.git This will clone the given Git repository to the `bar` directory and install its dependencies. -### Import a package from an existing directory +#### create --submodule: Manage existing packages by loading a Git repository as submodule + + $ studio create bar --submodule git@github.com:me/myrepo.git + +This will load the given Git repository to the `bar` directory as a submodule and install its dependencies. + +#### create --options: Provide specific options to Git when loading the repository + + $ studio create bar --git git@github.com:me/myrepo.git --options="--single-branch --branch=mybranch" + $ studio create bar --submodule git@github.com:me/myrepo.git --options="-b mybranch" - studio load baz +This will load the given Git repository and checkout a specific branch. +To have an overview of all the options available to you, check `git clone --help` and `git submodule add --help`. -This will make sure the package in the `baz` directory will be autoloadable using Composer. +#### load: Make all packages from the given local path available to Composer -### Remove a package + $ studio load baz + +This will make sure all packages in the `baz` directory (paths with wildcards are supported, too) will be autoloadable using Composer. + +#### unload: Stop managing a local path + + $ studio unload foo + +This will remove the path `foo` from the studio.json configuration file. +This means any packages in that path will not be available to Composer anymore (unless they are still hosted on Packagist). + +This does not remove the package contents from the file system. +See `scrap` for completely removing a package. + +You can reload the path using the `load` command. + +#### scrap: Remove a package Sometimes you want to throw away a package. You can do so with the `scrap` command, passing a path for a Studio-managed package: - studio scrap foo + $ studio scrap foo -Don't worry - you'll be asked for a confirmation first. +Don't worry - you'll be asked for confirmation first. ## License @@ -75,3 +175,9 @@ This means you can do almost anything with it, as long as the copyright notice a Feel free to send pull requests or create issues if you come across problems or have great ideas. Any input is appreciated! + +## franzl/studio for enterprise + +Available as part of the Tidelift Subscription + +The maintainers of franzl/studio and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-franzl-studio?utm_source=packagist-franzl-studio&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/bin/studio b/bin/studio index 8ece4be..95e1110 100755 --- a/bin/studio +++ b/bin/studio @@ -14,17 +14,17 @@ foreach ($autoloaders as $autoload) { } use Studio\Config\Config; -use Studio\Config\FileStorage; use Studio\Console\CreateCommand; use Studio\Console\LoadCommand; +use Studio\Console\UnloadCommand; use Studio\Console\ScrapCommand; use Symfony\Component\Console\Application; -$studioFile = getcwd().'/studio.json'; -$config = new Config(new FileStorage($studioFile)); +$config = Config::make(); -$application = new Application('studio', '0.10.0'); -$application->add(new CreateCommand($config)); +$application = new Application('studio', '0.15.0'); +$application->add(new CreateCommand); $application->add(new LoadCommand($config)); +$application->add(new UnloadCommand($config)); $application->add(new ScrapCommand($config)); $application->run(); diff --git a/composer.json b/composer.json index 06b3c11..499fc1e 100644 --- a/composer.json +++ b/composer.json @@ -10,25 +10,27 @@ } }, "require": { - "composer-plugin-api": "^1.0", - "illuminate/support": "~5.0", - "symfony/console": "^2.7|^3.0", - "symfony/finder": "^2.5|^3.0", - "symfony/filesystem": "^2.5|^3.0", - "symfony/process": "^2.5|^3.0" + "php": "^7.0 || ^8.0", + "composer-plugin-api": "^1.0 || ^2.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { - "composer/composer": "dev-master", - "phpspec/phpspec": "~2.3" + "composer/composer": "^2.4.2", + "phpspec/phpspec": "^6.3 || ^7.0" }, "replace": { "franzliedke/studio": "self.version" }, "extra": { "branch-alias": { - "dev-master": "0.10.x-dev" + "dev-master": "0.11.x-dev" }, "class": "Studio\\Composer\\StudioPlugin" }, - "bin": ["bin/studio"] + "bin": ["bin/studio"], + "scripts": { + "test": "phpspec run" + } } diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..e3b2291 --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,4 @@ +suites: + studio_suite: + namespace: Studio + psr4_prefix: Studio \ No newline at end of file diff --git a/spec/Config/Version2SerializerSpec.php b/spec/Config/Version2SerializerSpec.php new file mode 100644 index 0000000..9e64e20 --- /dev/null +++ b/spec/Config/Version2SerializerSpec.php @@ -0,0 +1,25 @@ +shouldHaveType('Studio\Config\Version2Serializer'); + } + + function it_stores_paths_alphabetically() + { + $this->serializePaths(['foo', 'bar'])->shouldReturn(['paths' => ['bar', 'foo']]); + } + + function it_deduplicates_paths() + { + // return array should have no gaps + $this->serializePaths(['bar', 'foo', 'test', 'foo'])->shouldReturn(['paths' => [0 => 'bar', 1 => 'foo', 2 => 'test']]); + } +} diff --git a/src/Composer/StudioPlugin.php b/src/Composer/StudioPlugin.php index 482fe92..54c7f15 100644 --- a/src/Composer/StudioPlugin.php +++ b/src/Composer/StudioPlugin.php @@ -7,10 +7,8 @@ use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; use Composer\Repository\PathRepository; -use Composer\Script\Event; use Composer\Script\ScriptEvents; use Studio\Config\Config; -use Studio\Config\FileStorage; class StudioPlugin implements PluginInterface, EventSubscriberInterface { @@ -24,17 +22,20 @@ class StudioPlugin implements PluginInterface, EventSubscriberInterface */ protected $io; - /** - * @var string|null - */ - protected $targetDir; - public function activate(Composer $composer, IOInterface $io) { $this->composer = $composer; $this->io = $io; } + public function deactivate(Composer $composer, IOInterface $io) + { + } + + public function uninstall(Composer $composer, IOInterface $io) + { + } + public static function getSubscribedEvents() { return [ @@ -43,37 +44,51 @@ public static function getSubscribedEvents() ]; } - public function registerStudioPackages(Event $event) + /** + * Register all managed paths with Composer. + * + * This function configures Composer to treat all Studio-managed paths as local path repositories, so that packages + * therein will be symlinked directly. + */ + public function registerStudioPackages() { - $this->targetDir = realpath($event->getComposer()->getPackage()->getTargetDir()); - $studioFile = "{$this->targetDir}/studio.json"; - - $config = $this->getConfig($studioFile); + $repoManager = $this->composer->getRepositoryManager(); + $composerConfig = $this->composer->getConfig(); - if ($config->hasPackages()) { - $io = $event->getIO(); - $repoManager = $event->getComposer()->getRepositoryManager(); - $composerConfig = $event->getComposer()->getConfig(); + foreach ($this->getManagedPaths() as $path) { + $this->io->writeError("[Studio] Loading path $path"); - foreach ($config->getPackages() as $package => $path) { - $io->writeError("[Studio] Registering package $package with $path"); + // Composer v2 always exposes the internal loop, so keep reusing it + // that is a fixed requirement since Composer >= 2.3 + if (method_exists($this->composer, 'getLoop')) { + $repoManager->prependRepository(new PathRepository( + ['url' => $path], + $this->io, + $composerConfig, + $this->composer->getLoop()->getHttpDownloader(), + $this->composer->getEventDispatcher(), + $this->composer->getLoop()->getProcessExecutor() + )); + } else { $repoManager->prependRepository(new PathRepository( - ['url' => $path], - $io, - $composerConfig - )); + ['url' => $path], + $this->io, + $composerConfig + )); } } } /** - * Instantiate and return the config object. + * Get the list of paths that are being managed by Studio. * - * @param string $file - * @return Config + * @return array */ - protected function getConfig($file) + private function getManagedPaths() { - return new Config(new FileStorage($file)); + $targetDir = realpath($this->composer->getPackage()->getTargetDir() ?? ''); + $config = Config::make("{$targetDir}/studio.json"); + + return $config->getPaths(); } } diff --git a/src/Config/Config.php b/src/Config/Config.php index 8e1aed9..821c11c 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -7,57 +7,117 @@ class Config { /** - * @var StorageInterface + * @var Serializer */ - protected $storage; + protected $serializer; - protected $packages; + protected $paths; protected $loaded = false; + protected $file; - public function __construct(StorageInterface $storage) + + public function __construct($file, Serializer $serializer) + { + $this->file = $file; + $this->serializer = $serializer; + } + + public static function make($file = null) + { + if (is_null($file)) { + $file = getcwd().'/studio.json'; + } + + return new static( + $file, + VersionedSerializer + ::withDefault(1, new Version1Serializer) + ->version(2, new Version2Serializer) + ); + } + + protected function readPaths() { - $this->storage = $storage; + if (!file_exists($this->file)) return []; + + $data = $this->readFromFile(); + return $this->serializer->deserializePaths($data); } - public function getPackages() + public function getPaths() { if (! $this->loaded) { - $this->packages = $this->storage->load(); + $this->paths = $this->readPaths(); $this->loaded = true; } - return $this->packages; + return $this->paths; + } + + public function addPath($path) + { + // Ensure paths are loaded + $this->getPaths(); + + $this->paths[] = $path; + $this->dump(); } - public function addPackage(Package $package) + public function removePath($path) { - // Ensure our packages are loaded - $this->getPackages(); + // Ensure paths are loaded + $this->getPaths(); + + $this->paths = array_filter($this->paths, function ($existing) use ($path) { + return $existing !== $path; + }); - $this->packages[$package->getComposerId()] = $package->getPath(); - $this->storage->store($this->packages); + $this->dump(); } public function hasPackages() { - // Ensure our packages are loaded - $this->getPackages(); + // Ensure paths are loaded + $this->getPaths(); - return ! empty($this->packages); + return ! empty($this->paths); } public function removePackage(Package $package) { - // Ensure our packages are loaded - $this->getPackages(); + // Ensure paths are loaded + $this->getPaths(); - $key = $package->getComposerId(); + $path = $package->getPath(); - if (isset($this->packages[$key])) { - unset($this->packages[$key]); - $this->storage->store($this->packages); + if (($key = array_search($path, $this->paths)) !== false) { + unset($this->paths[$key]); + $this->dump(); } } -} \ No newline at end of file + + protected function dump() + { + $this->writeToFile( + $this->serializer->serializePaths($this->paths) + ); + } + + protected function writeToFile(array $data) + { + file_put_contents( + $this->file, + json_encode( + $data, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + )."\n" + ); + } + + protected function readFromFile() + { + return json_decode(file_get_contents($this->file), true); + } +} diff --git a/src/Config/FileStorage.php b/src/Config/FileStorage.php deleted file mode 100644 index f4098c9..0000000 --- a/src/Config/FileStorage.php +++ /dev/null @@ -1,43 +0,0 @@ -file = $file; - } - - public function store($packages) - { - $this->writeToFile(['packages' => $packages]); - } - - public function load() - { - if (!file_exists($this->file)) return []; - - $contents = $this->readFromFile(); - return $contents['packages']; - } - - protected function writeToFile(array $data) - { - file_put_contents( - $this->file, - json_encode( - $data, - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - )."\n" - ); - } - - protected function readFromFile() - { - return json_decode(file_get_contents($this->file), true); - } -} diff --git a/src/Config/Serializer.php b/src/Config/Serializer.php new file mode 100644 index 0000000..e178610 --- /dev/null +++ b/src/Config/Serializer.php @@ -0,0 +1,10 @@ +getComposerId()] = $package->getPath(); + return $collect; + }, []); + + return ['packages' => $packagePaths]; + } +} diff --git a/src/Config/Version2Serializer.php b/src/Config/Version2Serializer.php new file mode 100644 index 0000000..8c653d9 --- /dev/null +++ b/src/Config/Version2Serializer.php @@ -0,0 +1,17 @@ + array_values(array_unique($paths))]; + } +} diff --git a/src/Config/VersionedSerializer.php b/src/Config/VersionedSerializer.php new file mode 100644 index 0000000..90c5991 --- /dev/null +++ b/src/Config/VersionedSerializer.php @@ -0,0 +1,74 @@ + $serializer], $version); + } + + public function __construct(array $serializers, $defaultVersion) + { + $this->serializers = $serializers; + $this->defaultVersion = $defaultVersion; + } + + public function version($version, Serializer $serializer) + { + $this->serializers[$version] = $serializer; + + return $this; + } + + public function deserializePaths($obj) + { + if (!isset($obj['version'])) { + $serializer = $this->serializers[$this->defaultVersion]; + } else if (array_key_exists(intval($obj['version']), $this->serializers)) { + $serializer = $this->serializers[$obj['version']]; + } else { + throw new \InvalidArgumentException('Invalid version'); + } + + return $serializer->deserializePaths($obj); + } + + public function serializePaths(array $paths) + { + $lastVersion = max(array_keys($this->serializers)); + $serializer = $this->serializers[$lastVersion]; + + return ['version' => $lastVersion] + $serializer->serializePaths($paths); + } +} diff --git a/src/Console/BaseCommand.php b/src/Console/BaseCommand.php index d0eac37..e8a2a87 100644 --- a/src/Console/BaseCommand.php +++ b/src/Console/BaseCommand.php @@ -10,29 +10,35 @@ abstract class BaseCommand extends Command { - /** * @var \Symfony\Component\Console\Input\InputInterface */ protected $input; /** - * @var \Symfony\Component\Console\Style\StyleInterface + * @var \Symfony\Component\Console\Output\OutputInterface */ protected $output; + /** + * @var \Symfony\Component\Console\Style\StyleInterface + */ + protected $io; + protected function execute(InputInterface $input, OutputInterface $output) { $this->input = $input; - $this->output = new SymfonyStyle($input, $output); + $this->output = $output; + $this->io = new SymfonyStyle($input, $output); try { $this->fire(); + return 0; } catch (Exception $e) { - $this->output->error($e->getMessage()); + $this->io->error($e->getMessage()); + return 1; } } abstract protected function fire(); - } diff --git a/src/Console/CreateCommand.php b/src/Console/CreateCommand.php index 498ff09..e541556 100644 --- a/src/Console/CreateCommand.php +++ b/src/Console/CreateCommand.php @@ -4,20 +4,17 @@ use Studio\Parts\ConsoleInput; use Studio\Shell\Shell; -use Studio\Config\Config; use Studio\Creator\CreatorInterface; use Studio\Creator\GitRepoCreator; use Studio\Creator\GitSubmoduleCreator; use Studio\Creator\SkeletonCreator; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; class CreateCommand extends BaseCommand { - - protected $config; - protected $partClasses = [ 'Studio\Parts\Base\Part', 'Studio\Parts\Composer\Part', @@ -32,13 +29,6 @@ class CreateCommand extends BaseCommand protected $partInput; - public function __construct(Config $config) - { - parent::__construct(); - - $this->config = $config; - } - protected function configure() { $this @@ -60,24 +50,36 @@ protected function configure() 'gs', InputOption::VALUE_REQUIRED, 'If set, this will download the given Git repository (as submodule) instead of creating a new one.' + ) + ->addOption( + 'options', + 'go', + InputOption::VALUE_REQUIRED, + 'If set, this will provide options to Git when fetching the repository or submodule.' ); } protected function fire() { - $this->partInput = new ConsoleInput($this->output); + $this->partInput = new ConsoleInput($this->io); $creator = $this->makeCreator($this->input); $package = $creator->create(); - $this->config->addPackage($package); $path = $package->getPath(); - $this->output->success("Package directory $path created."); + $this->io->success("Package directory $path created."); - $this->output->note('Running composer install for new package...'); + $this->io->note('Running composer install for new package...'); Shell::run('composer install --prefer-dist', $package->getPath()); - $this->output->success('Package successfully created.'); + $this->io->success('Package successfully created.'); + + if ($this->shouldLoadNewPackage()) { + $this->getApplication()->find('load')->run( + new ArrayInput(['path' => $path]), + $this->output + ); + } } /** @@ -91,14 +93,16 @@ protected function makeCreator(InputInterface $input) $path = $input->getArgument('path'); if ($input->getOption('git')) { - return new GitRepoCreator($input->getOption('git'), $path); - } elseif ($input->getOption('submodule')) { - return new GitSubmoduleCreator($input->getOption('submodule'), $path); - } else { - $creator = new SkeletonCreator($path); - $this->installParts($creator); - return $creator; + return new GitRepoCreator($input->getOption('git'), $path, $input->getOption('options')); } + + if ($input->getOption('submodule')) { + return new GitSubmoduleCreator($input->getOption('submodule'), $path, $input->getOption('options')); + } + + $creator = new SkeletonCreator($path); + $this->installParts($creator); + return $creator; } protected function installParts(SkeletonCreator $creator) @@ -120,4 +124,17 @@ protected function makeParts() }, $this->partClasses); } + protected function shouldLoadNewPackage() + { + if (!file_exists('composer.json')) { + return false; + } else if (!file_exists('studio.json')) { + return $this->io->confirm( + 'Do you want to load this package in the surrounding Composer package using Studio?', + true + ); + } else { + return true; + } + } } diff --git a/src/Console/LoadCommand.php b/src/Console/LoadCommand.php index e2563fd..dc5048a 100644 --- a/src/Console/LoadCommand.php +++ b/src/Console/LoadCommand.php @@ -2,14 +2,11 @@ namespace Studio\Console; -use Studio\Package; use Studio\Config\Config; -use Studio\Shell\Shell; use Symfony\Component\Console\Input\InputArgument; class LoadCommand extends BaseCommand { - protected $config; @@ -24,20 +21,20 @@ protected function configure() { $this ->setName('load') - ->setDescription('Load a package to be managed with Studio') + ->setDescription('Load a path to be managed with Studio') ->addArgument( 'path', - InputArgument::REQUIRED, - 'The path where the package files are located' + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'The path(s) where the package files are located' ); } protected function fire() { - $package = Package::fromFolder($this->input->getArgument('path')); - $this->config->addPackage($package); - - $this->output->success('Package loaded successfully.'); + foreach ($this->input->getArgument('path') as $path) { + $this->config->addPath($path); + + $this->io->success("Packages matching the path $path will now be loaded by Composer."); + } } - } diff --git a/src/Console/ScrapCommand.php b/src/Console/ScrapCommand.php index abb60a8..c5a6961 100644 --- a/src/Console/ScrapCommand.php +++ b/src/Console/ScrapCommand.php @@ -4,13 +4,11 @@ use Studio\Package; use Studio\Config\Config; -use Studio\Shell\Shell; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Filesystem\Filesystem; class ScrapCommand extends BaseCommand { - protected $config; @@ -38,27 +36,26 @@ protected function fire() $path = $this->input->getArgument('path'); if ($this->abortDeletion($path)) { - $this->output->note('Aborted.'); + $this->io->note('Aborted.'); return; } $package = Package::fromFolder($path); $this->config->removePackage($package); - $this->output->note('Removing package...'); + $this->io->note('Removing package...'); $filesystem = new Filesystem; $filesystem->remove($path); - $this->output->success('Package successfully removed.'); + $this->io->success('Package successfully removed.'); } protected function abortDeletion($path) { - $this->output->caution("This will delete the entire $path folder and all files within."); + $this->io->caution("This will delete the entire $path folder and all files within."); - return ! $this->output->confirm( + return ! $this->io->confirm( "Do you really want to scrap the package at $path? ", false ); } - } diff --git a/src/Console/UnloadCommand.php b/src/Console/UnloadCommand.php new file mode 100644 index 0000000..1d1aff3 --- /dev/null +++ b/src/Console/UnloadCommand.php @@ -0,0 +1,39 @@ +config = $config; + } + + protected function configure() + { + $this + ->setName('unload') + ->setDescription('Unload a package path from being managed with Studio') + ->addArgument( + 'path', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'The path(s) where the package files are located' + ); + } + + protected function fire() + { + foreach ($this->input->getArgument('path') as $path) { + $this->config->removePath($path); + + $this->io->success("Packages matching the path $path will no longer be loaded by Composer."); + } + } +} diff --git a/src/Creator/CreatorInterface.php b/src/Creator/CreatorInterface.php index 0235683..2dd1e11 100644 --- a/src/Creator/CreatorInterface.php +++ b/src/Creator/CreatorInterface.php @@ -4,12 +4,10 @@ interface CreatorInterface { - /** * Create the new package. * * @return \Studio\Package */ public function create(); - -} \ No newline at end of file +} diff --git a/src/Creator/GitRepoCreator.php b/src/Creator/GitRepoCreator.php index 0107367..d9b4200 100644 --- a/src/Creator/GitRepoCreator.php +++ b/src/Creator/GitRepoCreator.php @@ -7,16 +7,18 @@ class GitRepoCreator implements CreatorInterface { - protected $repo; protected $path; + protected $options; + - public function __construct($repo, $path) + public function __construct($repo, $path, $options = '') { $this->repo = $repo; $this->path = $path; + $this->options = $options; } /** @@ -33,7 +35,6 @@ public function create() protected function cloneRepository() { - Shell::run("git clone $this->repo $this->path"); + Shell::run("git clone $this->options $this->repo $this->path"); } - } diff --git a/src/Creator/GitSubmoduleCreator.php b/src/Creator/GitSubmoduleCreator.php index a0a4c85..f576719 100644 --- a/src/Creator/GitSubmoduleCreator.php +++ b/src/Creator/GitSubmoduleCreator.php @@ -6,11 +6,9 @@ class GitSubmoduleCreator extends GitRepoCreator { - protected function cloneRepository() { - Shell::run("git submodule add $this->repo $this->path"); + Shell::run("git submodule add $this->options $this->repo $this->path"); Shell::run("git submodule init"); } - } diff --git a/src/Creator/SkeletonCreator.php b/src/Creator/SkeletonCreator.php index 61dcfef..43d0e1b 100644 --- a/src/Creator/SkeletonCreator.php +++ b/src/Creator/SkeletonCreator.php @@ -8,7 +8,6 @@ class SkeletonCreator implements CreatorInterface { - /** * @var string */ @@ -64,5 +63,4 @@ protected function installParts() ) ); } - } diff --git a/src/Package.php b/src/Package.php index af20541..dafb49d 100644 --- a/src/Package.php +++ b/src/Package.php @@ -6,7 +6,6 @@ class Package { - protected $vendor; protected $name; @@ -30,11 +29,7 @@ public static function fromFolder($path) list($vendor, $name) = explode('/', $composer->name, 2); - return new static( - $vendor, - $name, - $path - ); + return new static($vendor, $name, $path); } public function __construct($vendor, $name, $path) @@ -63,5 +58,4 @@ public function getPath() { return $this->path; } - } diff --git a/src/Parts/AbstractPart.php b/src/Parts/AbstractPart.php index be3be4a..06d3fbb 100644 --- a/src/Parts/AbstractPart.php +++ b/src/Parts/AbstractPart.php @@ -7,7 +7,6 @@ abstract class AbstractPart implements PartInterface { - /** * @var PartInputInterface */ @@ -35,5 +34,4 @@ protected function copyTo($file, Directory $target, $targetName = null, Closure $target->write($targetName, $content); } - } diff --git a/src/Parts/Base/Part.php b/src/Parts/Base/Part.php index 9eee6b7..8c9b5cb 100644 --- a/src/Parts/Base/Part.php +++ b/src/Parts/Base/Part.php @@ -7,7 +7,6 @@ class Part extends AbstractPart { - public function setupPackage($composer, Directory $target) { $target->makeDir('src'); @@ -15,5 +14,4 @@ public function setupPackage($composer, Directory $target) $this->copyTo(__DIR__ . '/stubs/gitignore.txt', $target, '.gitignore'); } - } diff --git a/src/Parts/Composer/Part.php b/src/Parts/Composer/Part.php index ed69305..efc7a75 100644 --- a/src/Parts/Composer/Part.php +++ b/src/Parts/Composer/Part.php @@ -7,7 +7,6 @@ class Part extends AbstractPart { - public function setupPackage($composer, Directory $target) { // Ask for package name @@ -26,6 +25,7 @@ public function setupPackage($composer, Directory $target) ); // Normalize and store the namespace + $namespace = str_replace('/', '\\', $namespace); $namespace = rtrim($namespace, '\\'); @$composer->autoload->{'psr-4'}->{"$namespace\\"} = 'src/'; @@ -46,5 +46,4 @@ protected function makeDefaultNamespace($package) return ucfirst($vendor) . '\\' . ucfirst($name); } - } diff --git a/src/Parts/ConsoleInput.php b/src/Parts/ConsoleInput.php index 7080137..d80f10b 100644 --- a/src/Parts/ConsoleInput.php +++ b/src/Parts/ConsoleInput.php @@ -6,7 +6,6 @@ class ConsoleInput implements PartInputInterface { - /** * @var StyleInterface */ @@ -45,5 +44,4 @@ protected function validateWith($regex, $errorText) throw new \RuntimeException($errorText); }; } - } diff --git a/src/Parts/License/Part.php b/src/Parts/License/Part.php new file mode 100644 index 0000000..81e6694 --- /dev/null +++ b/src/Parts/License/Part.php @@ -0,0 +1,34 @@ +input->confirm('Do you want to configure a license for your project?')) { + $licenses = new SpdxLicenses(); + + $license = $this->selectLicenseFromList($licenses); + + $this->copyLicenseFileTo($target, $license); + + $composer->license = $license; + } + } + + protected function selectLicenseFromList($licenses) + { + // Ask the user to chosse a license from the list + } + + protected function copyLicenseFileTo(Directory $target, $license) + { + // Download the file + // Add year and name + } +} diff --git a/src/Parts/PartInputInterface.php b/src/Parts/PartInputInterface.php index 98fdcc6..73ae6f6 100644 --- a/src/Parts/PartInputInterface.php +++ b/src/Parts/PartInputInterface.php @@ -4,9 +4,7 @@ interface PartInputInterface { - public function confirm($question); public function ask($question, $regex, $errorText = null, $default = null); - } diff --git a/src/Parts/PartInterface.php b/src/Parts/PartInterface.php index 769f759..08d0e33 100644 --- a/src/Parts/PartInterface.php +++ b/src/Parts/PartInterface.php @@ -6,7 +6,5 @@ interface PartInterface { - public function setupPackage($composer, Directory $target); - } diff --git a/src/Parts/PhpSpec/Part.php b/src/Parts/PhpSpec/Part.php index 1372487..ed92c55 100644 --- a/src/Parts/PhpSpec/Part.php +++ b/src/Parts/PhpSpec/Part.php @@ -7,13 +7,13 @@ class Part extends AbstractPart { - public function setupPackage($composer, Directory $target) { if ($this->input->confirm('Do you want to set up PhpSpec as a testing tool?')) { - $composer->{'require-dev'}['phpspec/phpspec'] = '~2.0'; + $composer->{'require-dev'}['phpspec/phpspec'] = '^4.0'; - $namespace = head(array_keys((array) $composer->autoload->{'psr-4'})); + $psr4Autoloading = (array) $composer->autoload->{'psr-4'}; + $namespace = key($psr4Autoloading).'Tests'; $namespace = rtrim($namespace, '\\'); $this->copyTo( @@ -28,5 +28,4 @@ function ($content) use ($namespace) { $target->makeDir('spec'); } } - } diff --git a/src/Parts/PhpUnit/Part.php b/src/Parts/PhpUnit/Part.php index 92b16de..ab5185c 100644 --- a/src/Parts/PhpUnit/Part.php +++ b/src/Parts/PhpUnit/Part.php @@ -7,15 +7,14 @@ class Part extends AbstractPart { - public function setupPackage($composer, Directory $target) { if ($this->input->confirm('Do you want to set up PhpUnit as a testing tool?')) { - $composer->{'require-dev'}['phpunit/phpunit'] = '4.*'; + $composer->{'require-dev'}['phpunit/phpunit'] = '^6.3'; // Add autoloading rules for tests - $namespace = head(array_keys((array) $composer->autoload->{'psr-4'})); - $namespace .= 'Tests'; + $psr4Autoloading = (array) $composer->autoload->{'psr-4'}; + $namespace = key($psr4Autoloading).'Tests'; @$composer->{'autoload-dev'}->{'psr-4'}->{"$namespace\\"} = 'tests/'; @@ -32,5 +31,4 @@ function ($content) use ($namespace) { $this->copyTo(__DIR__ . '/stubs/phpunit.xml', $target); } } - } diff --git a/src/Parts/PhpUnit/stubs/tests/ExampleTest.php b/src/Parts/PhpUnit/stubs/tests/ExampleTest.php old mode 100644 new mode 100755 index 284d79b..b49327c --- a/src/Parts/PhpUnit/stubs/tests/ExampleTest.php +++ b/src/Parts/PhpUnit/stubs/tests/ExampleTest.php @@ -2,7 +2,9 @@ namespace Foo\Bar\Tests; -class ExampleTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class ExampleTest extends TestCase { } diff --git a/src/Parts/TravisCI/Part.php b/src/Parts/TravisCI/Part.php index ce612ff..4e3aa27 100644 --- a/src/Parts/TravisCI/Part.php +++ b/src/Parts/TravisCI/Part.php @@ -7,12 +7,10 @@ class Part extends AbstractPart { - public function setupPackage($composer, Directory $target) { if ($this->input->confirm('Do you want to set up TravisCI as continuous integration tool?')) { $this->copyTo(__DIR__ . '/stubs/.travis.yml', $target); } } - } diff --git a/src/Parts/TravisCI/stubs/.travis.yml b/src/Parts/TravisCI/stubs/.travis.yml index f60bbe0..ac80f2d 100644 --- a/src/Parts/TravisCI/stubs/.travis.yml +++ b/src/Parts/TravisCI/stubs/.travis.yml @@ -1,9 +1,9 @@ language: php php: - - 5.4 - 5.5 - 5.6 + - 7.0 - hhvm before_script: diff --git a/src/Shell/Shell.php b/src/Shell/Shell.php index 56d80d6..d670c1c 100644 --- a/src/Shell/Shell.php +++ b/src/Shell/Shell.php @@ -2,25 +2,38 @@ namespace Studio\Shell; +use ReflectionClass; +use RuntimeException; use Symfony\Component\Process\Process; class Shell { - public static function run($task, $directory = null) { - $process = new Process("$task", $directory); + $process = self::makeProcess($task, $directory); $process->setTimeout(3600); $process->run(); if (! $process->isSuccessful()) { - $command = collect(explode(' ', $task))->first(); + $command = preg_replace('/ .+$/', '', $task); $error = $process->getErrorOutput(); - throw new \RuntimeException("Error while running $command: $error"); + throw new RuntimeException("Error while running $command: $error"); } return $process->getOutput(); } + private static function makeProcess($task, $directory) + { + $reflection = new ReflectionClass(Process::class); + $params = $reflection->getConstructor()->getParameters(); + $type = $params[0]->getType(); + + if ($type && $type->getName() === 'array') { // Symfony 5 + return new Process(explode(' ', $task), $directory); + } else { // Older versions + return new Process($task, $directory); + } + } }