Add and use an action to get the project language

This commit is contained in:
Oliver Davies 2024-02-22 21:04:40 +00:00
parent 8c9f36151b
commit ccbae0ed36
17 changed files with 2328 additions and 51 deletions

6
.env.test Normal file
View file

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

11
.gitignore vendored
View file

@ -1,3 +1,4 @@
/bin/
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local
@ -16,3 +17,13 @@
###> phpstan/phpstan ### ###> phpstan/phpstan ###
phpstan.neon phpstan.neon
###< phpstan/phpstan ### ###< phpstan/phpstan ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

View file

@ -8,6 +8,10 @@ flake:
- php82 - php82
- php82Packages.composer - php82Packages.composer
git:
ignore:
- /bin/
# experimental: # experimental:
# TODO: not yet supported in Symfony projects? # TODO: not yet supported in Symfony projects?
# createInclusiveGitIgnoreFile: true # createInclusiveGitIgnoreFile: true

View file

@ -70,6 +70,10 @@
"bamarni/composer-bin-plugin": "^1.8", "bamarni/composer-bin-plugin": "^1.8",
"phpstan/extension-installer": "^1.3", "phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpstan/phpstan-strict-rules": "^1.5" "phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.0.*",
"symfony/css-selector": "7.0.*",
"symfony/phpunit-bridge": "^7.0"
} }
} }

2084
composer.lock generated

File diff suppressed because it is too large Load diff

2
notes
View file

@ -1,4 +1,4 @@
Keep adding JS/TS support Keep adding JS/TS support
Review https://github.com/phpstan/phpstan-symfony Review https://github.com/phpstan/phpstan-symfony
Add tests for determining the correct project language and type based on present files Add tests for determining the correct project type based on present files
Add documentation for each project language and type - table of command that are run? Add documentation for each project language and type - table of command that are run?

38
phpunit.xml.dist Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.6" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
</extensions>
</phpunit>

View file

@ -0,0 +1,36 @@
<?php
namespace App\Action;
use App\Enum\ProjectLanguage;
use Symfony\Component\Filesystem\Filesystem;
final class DetermineProjectLanguage
{
public function __construct(
private Filesystem $filesystem,
private string $workingDir = '.',
) {
}
/**
* @return non-empty-string
*/
public function getLanguage(): string
{
if ($this->filesystem->exists($this->workingDir.'/composer.json')) {
return ProjectLanguage::PHP->value;
}
if ($this->filesystem->exists($this->workingDir.'/package.json')) {
return ProjectLanguage::JavaScript->value;
}
// TODO: What to do if a project contains multiple languages?
// e.g. a composer.lock file (PHP) and pnpm-lock.yaml file (JS)?
// TODO: validate the language is an allowed value.
return ProjectLanguage::PHP->value;
}
}

View file

@ -4,9 +4,17 @@ namespace App\Console\Command;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Filesystem\Filesystem;
abstract class AbstractCommand extends Command abstract class AbstractCommand extends Command
{ {
public function __construct(
string $name,
protected Filesystem $filesystem,
) {
parent::__construct(name: $name);
}
protected function configure(): void protected function configure(): void
{ {
$this->addOption( $this->addOption(

View file

@ -2,6 +2,7 @@
namespace App\Console\Command; namespace App\Console\Command;
use App\Action\DetermineProjectLanguage;
use App\Enum\ProjectLanguage; use App\Enum\ProjectLanguage;
use App\Enum\ProjectType; use App\Enum\ProjectType;
use App\Process\Process; use App\Process\Process;
@ -15,20 +16,22 @@ final class BuildCommand extends AbstractCommand
{ {
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
$projectLanguage = null;
$projectType = null; $projectType = null;
$extraArgs = $input->getOption('extra-args'); $extraArgs = $input->getOption('extra-args');
$workingDir = $input->getOption('working-dir'); $workingDir = $input->getOption('working-dir');
$language = $input->getOption('language') ?? (new DetermineProjectLanguage(
filesystem: $this->filesystem,
workingDir: $workingDir,
))->getLanguage();
$filesystem = new Filesystem(); $filesystem = new Filesystem();
// Attempt to prepopulate some of the options, such as the project type // Attempt to prepopulate some of the options, such as the project type
// based on its dependencies. // based on its dependencies.
// TODO: move this logic to a service so it can be tested. // TODO: move this logic to a service so it can be tested.
if ($filesystem->exists($workingDir.'/composer.json')) { if ($language === ProjectLanguage::PHP->value) {
$projectLanguage = ProjectLanguage::PHP->value;
$json = json_decode( $json = json_decode(
json: strval(file_get_contents($workingDir.'/composer.json')), json: strval(file_get_contents($workingDir.'/composer.json')),
associative: true, associative: true,
@ -44,18 +47,17 @@ final class BuildCommand extends AbstractCommand
$projectType = ProjectType::Symfony->value; $projectType = ProjectType::Symfony->value;
} }
} elseif ($filesystem->exists($workingDir.'/fractal.config.js')) { } elseif ($filesystem->exists($workingDir.'/fractal.config.js')) {
$projectLanguage = ProjectLanguage::JavaScript->value; $language = ProjectLanguage::JavaScript->value;
$projectType = ProjectType::Fractal->value; $projectType = ProjectType::Fractal->value;
} }
// Even if the project language or type is found automatically, still // Even if the project type is found automatically, still override it
// override it with the option value if there is one. // with the option value if there is one.
$projectLanguage = $input->getOption('language') ?? $projectLanguage;
$projectType = $input->getOption('type') ?? $projectType; $projectType = $input->getOption('type') ?? $projectType;
$isDockerCompose = $filesystem->exists($workingDir . '/docker-compose.yaml'); $isDockerCompose = $filesystem->exists($workingDir . '/docker-compose.yaml');
switch ($projectLanguage) { switch ($language) {
case ProjectLanguage::PHP->value: case ProjectLanguage::PHP->value:
switch ($projectType) { switch ($projectType) {
case ProjectType::Drupal->value: case ProjectType::Drupal->value:
@ -83,7 +85,7 @@ final class BuildCommand extends AbstractCommand
$process->run(); $process->run();
break; break;
} }
case ProjectLanguage::JavaScript->value: case ProjectLanguage::JavaScript->value:
switch ($projectType) { switch ($projectType) {

View file

@ -2,6 +2,7 @@
namespace App\Console\Command; namespace App\Console\Command;
use App\Action\DetermineProjectLanguage;
use App\Enum\ProjectLanguage; use App\Enum\ProjectLanguage;
use App\Process\Process; use App\Process\Process;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -16,10 +17,10 @@ final class InstallCommand extends AbstractCommand
$extraArgs = $input->getOption('extra-args'); $extraArgs = $input->getOption('extra-args');
$workingDir = $input->getOption('working-dir'); $workingDir = $input->getOption('working-dir');
// TODO: What to do if a project contains multiple languages? $language = $input->getOption('language') ?? (new DetermineProjectLanguage(
// e.g. a composer.lock file (PHP) and pnpm-lock.yaml file (JS)? filesystem: $this->filesystem,
workingDir: $workingDir,
// TODO: validate the language is an allowed value. ))->getLanguage();
$filesystem = new Filesystem(); $filesystem = new Filesystem();
@ -27,7 +28,7 @@ final class InstallCommand extends AbstractCommand
$process = Process::create( $process = Process::create(
command: $this->getCommand( command: $this->getCommand(
filesystem: $filesystem, filesystem: $filesystem,
language: $this->getProjectLanguage($filesystem, $workingDir, $input), language: $language,
workingDir: $workingDir, workingDir: $workingDir,
), ),
extraArgs: explode(separator: ' ', string: $extraArgs), extraArgs: explode(separator: ' ', string: $extraArgs),
@ -60,23 +61,4 @@ final class InstallCommand extends AbstractCommand
return ['composer', 'install']; return ['composer', 'install'];
} }
/**
* @param Filesystem $filesystem
* @param non-empty-string $workingDir
* @param InputInterface $input
* @return non-empty-string
*/
private function getProjectLanguage(Filesystem $filesystem, string $workingDir, InputInterface $input): string {
$projectLanguage = null;
// Determine the language based on the files.
if ($filesystem->exists($workingDir.'/composer.json')) {
$projectLanguage = ProjectLanguage::PHP->value;
} elseif ($filesystem->exists($workingDir.'/package.json')) {
$projectLanguage = ProjectLanguage::JavaScript->value;
}
return $input->getOption('language') ?? $projectLanguage;
}
} }

View file

@ -2,6 +2,7 @@
namespace App\Console\Command; namespace App\Console\Command;
use App\Action\DetermineProjectLanguage;
use App\Enum\ProjectLanguage; use App\Enum\ProjectLanguage;
use App\Enum\ProjectType; use App\Enum\ProjectType;
use App\Process\Process; use App\Process\Process;
@ -14,7 +15,6 @@ final class RunCommand extends AbstractCommand
{ {
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
$projectLanguage = null;
$projectType = null; $projectType = null;
$extraArgs = $input->getOption('extra-args'); $extraArgs = $input->getOption('extra-args');
@ -22,12 +22,15 @@ final class RunCommand extends AbstractCommand
$filesystem = new Filesystem(); $filesystem = new Filesystem();
$language = $input->getOption('language') ?? (new DetermineProjectLanguage(
filesystem: $this->filesystem,
workingDir: $workingDir,
))->getLanguage();
// Attempt to prepopulate some of the options, such as the project type // Attempt to prepopulate some of the options, such as the project type
// based on its dependencies. // based on its dependencies.
// TODO: move this logic to a service so it can be tested. // TODO: move this logic to a service so it can be tested.
if ($filesystem->exists($workingDir.'/composer.json')) { if ($language === ProjectLanguage::PHP->value) {
$projectLanguage = ProjectLanguage::PHP->value;
$json = json_decode( $json = json_decode(
json: strval(file_get_contents($workingDir.'/composer.json')), json: strval(file_get_contents($workingDir.'/composer.json')),
associative: true, associative: true,
@ -42,17 +45,14 @@ final class RunCommand extends AbstractCommand
} elseif (in_array(needle: 'symfony/framework-bundle', haystack: $dependencies, strict: true)) { } elseif (in_array(needle: 'symfony/framework-bundle', haystack: $dependencies, strict: true)) {
$projectType = ProjectType::Symfony->value; $projectType = ProjectType::Symfony->value;
} }
} elseif ($filesystem->exists($workingDir.'/package.json')) { } elseif ($language === ProjectLanguage::JavaScript->value) {
$projectLanguage = ProjectLanguage::JavaScript->value;
if ($filesystem->exists($workingDir.'/fractal.config.js')) { if ($filesystem->exists($workingDir.'/fractal.config.js')) {
$projectType = ProjectType::Fractal->value; $projectType = ProjectType::Fractal->value;
} }
} }
// Even if the project type is found automatically, still override it with // Even if the project type is found automatically, still override it
// the option value if there is one. // with the option value if there is one.
$projectLanguage = $input->getOption('language') ?? $projectLanguage;
$projectType = $input->getOption('type') ?? $projectType; $projectType = $input->getOption('type') ?? $projectType;
$filesystem = new Filesystem(); $filesystem = new Filesystem();

View file

@ -14,6 +14,8 @@ final class TestCommand extends AbstractCommand
$extraArgs = $input->getOption('extra-args'); $extraArgs = $input->getOption('extra-args');
$workingDir = $input->getOption('working-dir'); $workingDir = $input->getOption('working-dir');
// TODO: add support for node and jest.
// TODO: move this logic to a service so it can be tested. // TODO: move this logic to a service so it can be tested.
$json = json_decode( $json = json_decode(
json: strval(file_get_contents($workingDir.'/composer.json')), json: strval(file_get_contents($workingDir.'/composer.json')),

View file

@ -11,6 +11,20 @@
"phpstan.dist.neon" "phpstan.dist.neon"
] ]
}, },
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/console": { "symfony/console": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {
@ -54,6 +68,21 @@
"src/Kernel.php" "src/Kernel.php"
] ]
}, },
"symfony/phpunit-bridge": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {

View file

@ -0,0 +1,58 @@
<?php
use App\Action\DetermineProjectLanguage;
use App\Enum\ProjectLanguage;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Filesystem\Filesystem;
final class ProjectLanguageTest extends TestCase
{
/**
* @test
* @testdox It identifies a PHP project if it has a composer.json file
*/
public function it_identifies_a_php_project_if_it_has_a_composer_json_file(): void
{
$filesystem = $this->createMock(Filesystem::class);
$filesystem
->method('exists')
->with('./composer.json')
->willReturn(true);
$action = new DetermineProjectLanguage(
filesystem: $filesystem,
);
self::assertSame(
actual: $action->getLanguage(),
expected: ProjectLanguage::PHP->value,
);
}
/**
* @test
* @testdox It identifies a node project if it has a package.json file
*/
public function it_identifies_a_node_project_if_it_has_a_package_json_file(): void
{
// self::markTestSkipped();
$filesystem = $this->createMock(Filesystem::class);
$filesystem
->method('exists')
->will(self::returnValueMap([
['./composer.json', false],
['./package.json', true],
]));
$action = new DetermineProjectLanguage(
filesystem: $filesystem,
);
self::assertSame(
actual: $action->getLanguage(),
expected: ProjectLanguage::JavaScript->value,
);
}
}

13
tests/bootstrap.php Normal file
View file

@ -0,0 +1,13 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) {
umask(0000);
}

10
versa
View file

@ -8,14 +8,16 @@ use App\Console\Command\InstallCommand;
use App\Console\Command\RunCommand; use App\Console\Command\RunCommand;
use App\Console\Command\TestCommand; use App\Console\Command\TestCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
$application = new Application(); $application = new Application();
$filesystem = new Filesystem();
$application->addCommands([ $application->addCommands([
new BuildCommand(name: 'build'), new BuildCommand(filesystem: $filesystem, name: 'build'),
new InstallCommand(name: 'install'), new InstallCommand(filesystem: $filesystem, name: 'install'),
new RunCommand(name: 'run'), new RunCommand(filesystem: $filesystem, name: 'run'),
new TestCommand(name: 'test'), new TestCommand(filesystem: $filesystem, name: 'test'),
]); ]);
$application->run(); $application->run();