welcome back to dyb-tech
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use Composer\Semver\Semver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\MakerBundle\MakerInterface;
|
||||
use Symfony\Bundle\MakerBundle\Str;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
abstract class MakerTestCase extends TestCase
|
||||
{
|
||||
private ?KernelInterface $kernel = null;
|
||||
|
||||
/**
|
||||
* @dataProvider getTestDetails
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testExecute(MakerTestDetails $makerTestDetails)
|
||||
{
|
||||
$this->executeMakerCommand($makerTestDetails);
|
||||
}
|
||||
|
||||
abstract public function getTestDetails();
|
||||
|
||||
abstract protected function getMakerClass(): string;
|
||||
|
||||
protected function createMakerTest(): MakerTestDetails
|
||||
{
|
||||
return new MakerTestDetails($this->getMakerInstance($this->getMakerClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function executeMakerCommand(MakerTestDetails $testDetails)
|
||||
{
|
||||
if (!class_exists(Process::class)) {
|
||||
throw new \LogicException('The MakerTestCase cannot be run as the Process component is not installed. Try running "compose require --dev symfony/process".');
|
||||
}
|
||||
|
||||
if (!$testDetails->isSupportedByCurrentPhpVersion()) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
$testEnv = MakerTestEnvironment::create($testDetails);
|
||||
|
||||
if ('7.0.x-dev' === $testEnv->getTargetSkeletonVersion() && $testDetails->getSkipOnSymfony7()) {
|
||||
$this->markTestSkipped('This test is skipped on Symfony 7');
|
||||
}
|
||||
|
||||
// prepare environment to test
|
||||
$testEnv->prepareDirectory();
|
||||
|
||||
if (!$this->hasRequiredDependencyVersions($testDetails, $testEnv)) {
|
||||
$this->markTestSkipped('Some dependencies versions are too low');
|
||||
}
|
||||
|
||||
$makerRunner = new MakerTestRunner($testEnv);
|
||||
foreach ($testDetails->getPreRunCallbacks() as $preRunCallback) {
|
||||
$preRunCallback($makerRunner);
|
||||
}
|
||||
|
||||
$callback = $testDetails->getRunCallback();
|
||||
$callback($makerRunner);
|
||||
|
||||
// run tests
|
||||
$files = $testEnv->getGeneratedFilesFromOutputText();
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->assertTrue($testEnv->fileExists($file), sprintf('The file "%s" does not exist after generation', $file));
|
||||
|
||||
if (str_ends_with($file, '.twig')) {
|
||||
$csProcess = $testEnv->runTwigCSLint($file);
|
||||
|
||||
$this->assertTrue($csProcess->isSuccessful(), sprintf('File "%s" has a twig-cs problem: %s', $file, $csProcess->getErrorOutput()."\n".$csProcess->getOutput()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function assertContainsCount(string $needle, string $haystack, int $count)
|
||||
{
|
||||
$this->assertEquals(1, substr_count($haystack, $needle), sprintf('Found more than %d occurrences of "%s" in "%s"', $count, $needle, $haystack));
|
||||
}
|
||||
|
||||
private function getMakerInstance(string $makerClass): MakerInterface
|
||||
{
|
||||
if (null === $this->kernel) {
|
||||
$this->kernel = $this->createKernel();
|
||||
$this->kernel->boot();
|
||||
}
|
||||
|
||||
// a cheap way to guess the service id
|
||||
$serviceId ??= sprintf('maker.maker.%s', Str::asSnakeCase((new \ReflectionClass($makerClass))->getShortName()));
|
||||
|
||||
return $this->kernel->getContainer()->get($serviceId);
|
||||
}
|
||||
|
||||
protected function createKernel(): KernelInterface
|
||||
{
|
||||
return new MakerTestKernel('dev', true);
|
||||
}
|
||||
|
||||
private function hasRequiredDependencyVersions(MakerTestDetails $testDetails, MakerTestEnvironment $testEnv): bool
|
||||
{
|
||||
if (empty($testDetails->getRequiredPackageVersions())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$installedPackages = json_decode($testEnv->readFile('vendor/composer/installed.json'), true, 512, \JSON_THROW_ON_ERROR);
|
||||
$packageVersions = [];
|
||||
|
||||
foreach ($installedPackages['packages'] ?? $installedPackages as $installedPackage) {
|
||||
$packageVersions[$installedPackage['name']] = $installedPackage['version_normalized'];
|
||||
}
|
||||
|
||||
foreach ($testDetails->getRequiredPackageVersions() as $requiredPackageData) {
|
||||
$name = $requiredPackageData['name'];
|
||||
$versionConstraint = $requiredPackageData['version_constraint'];
|
||||
|
||||
if (!isset($packageVersions[$name])) {
|
||||
throw new \Exception(sprintf('Package "%s" is required in the test project at version "%s" but it is not installed?', $name, $versionConstraint));
|
||||
}
|
||||
|
||||
if (!Semver::satisfies($packageVersions[$name], $versionConstraint)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use Symfony\Bundle\MakerBundle\DependencyBuilder;
|
||||
use Symfony\Bundle\MakerBundle\MakerInterface;
|
||||
|
||||
final class MakerTestDetails
|
||||
{
|
||||
private ?\Closure $runCallback = null;
|
||||
private array $preRunCallbacks = [];
|
||||
private array $extraDependencies = [];
|
||||
private string $rootNamespace = 'App';
|
||||
private int $requiredPhpVersion = 80000;
|
||||
private array $requiredPackageVersions = [];
|
||||
private int $blockedPhpVersionUpper = 0;
|
||||
private int $blockedPhpVersionLower = 0;
|
||||
private bool $skipOnSymfony7 = false;
|
||||
|
||||
public function __construct(
|
||||
private MakerInterface $maker,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(\Closure $callback): self
|
||||
{
|
||||
$this->runCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function preRun(\Closure $callback): self
|
||||
{
|
||||
$this->preRunCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRootNamespace()
|
||||
{
|
||||
return $this->rootNamespace;
|
||||
}
|
||||
|
||||
public function changeRootNamespace(string $rootNamespace): self
|
||||
{
|
||||
$this->rootNamespace = trim($rootNamespace, '\\');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addExtraDependencies(string ...$packages): self
|
||||
{
|
||||
$this->extraDependencies += $packages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRequiredPhpVersion(int $version): self
|
||||
{
|
||||
@trigger_deprecation('symfony/maker-bundle', 'v1.44.0', 'setRequiredPhpVersion() is no longer used and will be removed in a future version.');
|
||||
|
||||
$this->requiredPhpVersion = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a test from running between a range of PHP Versions.
|
||||
*
|
||||
* @param int $lowerLimit Versions below this value will be allowed
|
||||
* @param int $upperLimit Versions above this value will be allowed
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setSkippedPhpVersions(int $lowerLimit, int $upperLimit): self
|
||||
{
|
||||
$this->blockedPhpVersionUpper = $upperLimit;
|
||||
$this->blockedPhpVersionLower = $lowerLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addRequiredPackageVersion(string $packageName, string $versionConstraint): self
|
||||
{
|
||||
$this->requiredPackageVersions[] = ['name' => $packageName, 'version_constraint' => $versionConstraint];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUniqueCacheDirectoryName(): string
|
||||
{
|
||||
// for cache purposes, only the dependencies are important!
|
||||
// You can change it ONLY if you don't have another way to implement it
|
||||
return 'maker_'.strtolower($this->getRootNamespace()).'_'.md5(serialize($this->getDependencies()));
|
||||
}
|
||||
|
||||
public function getMaker(): MakerInterface
|
||||
{
|
||||
return $this->maker;
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
$depBuilder = $this->getDependencyBuilder();
|
||||
|
||||
return array_merge(
|
||||
$depBuilder->getAllRequiredDependencies(),
|
||||
$depBuilder->getAllRequiredDevDependencies(),
|
||||
$this->extraDependencies
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraDependencies(): array
|
||||
{
|
||||
return $this->extraDependencies;
|
||||
}
|
||||
|
||||
public function getDependencyBuilder(): DependencyBuilder
|
||||
{
|
||||
$depBuilder = new DependencyBuilder();
|
||||
$this->maker->configureDependencies($depBuilder);
|
||||
|
||||
return $depBuilder;
|
||||
}
|
||||
|
||||
public function isSupportedByCurrentPhpVersion(): bool
|
||||
{
|
||||
$hasPhpVersionConstraint = $this->blockedPhpVersionLower > 0 && $this->blockedPhpVersionUpper > 0;
|
||||
$isSupported = false;
|
||||
|
||||
if (!$hasPhpVersionConstraint) {
|
||||
$isSupported = true;
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID > $this->blockedPhpVersionUpper) {
|
||||
$isSupported = true;
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < $this->blockedPhpVersionLower) {
|
||||
$isSupported = true;
|
||||
}
|
||||
|
||||
return $isSupported && \PHP_VERSION_ID >= $this->requiredPhpVersion;
|
||||
}
|
||||
|
||||
public function getRequiredPackageVersions(): array
|
||||
{
|
||||
return $this->requiredPackageVersions;
|
||||
}
|
||||
|
||||
public function getRunCallback(): \Closure
|
||||
{
|
||||
if (!$this->runCallback) {
|
||||
throw new \Exception('Don\'t forget to call ->run()');
|
||||
}
|
||||
|
||||
return $this->runCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Closure[]
|
||||
*/
|
||||
public function getPreRunCallbacks(): array
|
||||
{
|
||||
return $this->preRunCallbacks;
|
||||
}
|
||||
|
||||
public function skipOnSymfony7(): self
|
||||
{
|
||||
$this->skipOnSymfony7 = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSkipOnSymfony7(): bool
|
||||
{
|
||||
return $this->skipOnSymfony7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Process\InputStream;
|
||||
|
||||
/**
|
||||
* @author Sadicov Vladimir <sadikoff@gmail.com>
|
||||
* @author Nicolas Philippe <nikophil@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class MakerTestEnvironment
|
||||
{
|
||||
private Filesystem $fs;
|
||||
private bool|string $rootPath;
|
||||
private string $cachePath;
|
||||
private string $flexPath;
|
||||
private string $path;
|
||||
private MakerTestProcess $runnedMakerProcess;
|
||||
private bool $isWindows;
|
||||
|
||||
private function __construct(
|
||||
private MakerTestDetails $testDetails,
|
||||
) {
|
||||
$this->isWindows = str_contains(strtolower(\PHP_OS), 'win');
|
||||
|
||||
$this->fs = new Filesystem();
|
||||
$this->rootPath = realpath(__DIR__.'/../../');
|
||||
$cachePath = $this->rootPath.'/tests/tmp/cache';
|
||||
|
||||
if (!$this->fs->exists($cachePath)) {
|
||||
$this->fs->mkdir($cachePath);
|
||||
}
|
||||
|
||||
$this->cachePath = realpath($cachePath);
|
||||
$targetVersion = $this->getTargetSkeletonVersion();
|
||||
$this->flexPath = $this->cachePath.'/flex_project'.$targetVersion;
|
||||
|
||||
$directoryName = $targetVersion ?: 'current';
|
||||
if (str_ends_with($directoryName, '.*')) {
|
||||
$directoryName = substr($directoryName, 0, -2);
|
||||
}
|
||||
|
||||
$this->path = $this->cachePath.\DIRECTORY_SEPARATOR.$testDetails->getUniqueCacheDirectoryName().'_'.$directoryName;
|
||||
}
|
||||
|
||||
public static function create(MakerTestDetails $testDetails): self
|
||||
{
|
||||
return new self($testDetails);
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function readFile(string $path): string
|
||||
{
|
||||
if (!file_exists($this->path.'/'.$path)) {
|
||||
throw new \InvalidArgumentException(sprintf('Cannot find file "%s"', $path));
|
||||
}
|
||||
|
||||
return file_get_contents($this->path.'/'.$path);
|
||||
}
|
||||
|
||||
private function changeRootNamespaceIfNeeded(): void
|
||||
{
|
||||
if ('App' === ($rootNamespace = $this->testDetails->getRootNamespace())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
[
|
||||
'filename' => 'composer.json',
|
||||
'find' => '"App\\\\": "src/"',
|
||||
'replace' => '"'.$rootNamespace.'\\\\": "src/"',
|
||||
],
|
||||
[
|
||||
'filename' => 'src/Kernel.php',
|
||||
'find' => 'namespace App',
|
||||
'replace' => 'namespace '.$rootNamespace,
|
||||
],
|
||||
[
|
||||
'filename' => 'bin/console',
|
||||
'find' => 'use App\\Kernel',
|
||||
'replace' => 'use '.$rootNamespace.'\\Kernel',
|
||||
],
|
||||
[
|
||||
'filename' => 'public/index.php',
|
||||
'find' => 'use App\\Kernel',
|
||||
'replace' => 'use '.$rootNamespace.'\\Kernel',
|
||||
],
|
||||
[
|
||||
'filename' => 'config/services.yaml',
|
||||
'find' => 'App\\',
|
||||
'replace' => $rootNamespace.'\\',
|
||||
],
|
||||
[
|
||||
'filename' => '.env.test',
|
||||
'find' => 'KERNEL_CLASS=\'App\Kernel\'',
|
||||
'replace' => 'KERNEL_CLASS=\''.$rootNamespace.'\Kernel\'',
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->fs->exists($this->path.'/config/packages/doctrine.yaml')) {
|
||||
$replacements[] = [
|
||||
'filename' => 'config/packages/doctrine.yaml',
|
||||
'find' => 'App',
|
||||
'replace' => $rootNamespace,
|
||||
];
|
||||
}
|
||||
|
||||
$this->processReplacements($replacements, $this->path);
|
||||
$this->runCommand('composer dump-autoload');
|
||||
}
|
||||
|
||||
public function prepareDirectory(): void
|
||||
{
|
||||
// Copy MakerBundle to a "repo" directory for tests
|
||||
if (!file_exists($makerRepoPath = sprintf('%s/maker-repo', $this->cachePath))) {
|
||||
MakerTestProcess::create(sprintf('git clone %s %s', $this->rootPath, $makerRepoPath), $this->cachePath)->run();
|
||||
}
|
||||
|
||||
if (!$this->fs->exists($this->flexPath)) {
|
||||
$this->buildFlexSkeleton();
|
||||
}
|
||||
|
||||
if (!$this->fs->exists($this->path)) {
|
||||
try {
|
||||
// let's do some magic here git is faster than copy
|
||||
MakerTestProcess::create(
|
||||
'\\' === \DIRECTORY_SEPARATOR ? 'git clone %FLEX_PATH% %APP_PATH%' : 'git clone "$FLEX_PATH" "$APP_PATH"',
|
||||
\dirname($this->flexPath),
|
||||
[
|
||||
'FLEX_PATH' => $this->flexPath,
|
||||
'APP_PATH' => $this->path,
|
||||
]
|
||||
)
|
||||
->run();
|
||||
|
||||
// In Window's we have to require MakerBundle in each project - git clone doesn't symlink well
|
||||
if ($this->isWindows) {
|
||||
$this->composerRequireMakerBundle($this->path);
|
||||
}
|
||||
|
||||
// install any missing dependencies
|
||||
$dependencies = $this->determineMissingDependencies();
|
||||
if ($dependencies) {
|
||||
// -v actually silences the "very" verbose output in case of an error
|
||||
$composerProcess = MakerTestProcess::create(sprintf('composer require %s -v', implode(' ', $dependencies)), $this->path);
|
||||
|
||||
// @legacy Temporary code until doctrine/dbal 3.7 is out (which supports symfony/console 7.0
|
||||
$composerProcess->run(true);
|
||||
if (!$composerProcess->isSuccessful()) {
|
||||
if (str_contains($composerProcess->getErrorOutput(), 'Declaration of Doctrine\DBAL\Tools\Console\Command\RunSqlCommand::execute')) {
|
||||
$this->patchDoctrineDbalForSymfony7();
|
||||
} else {
|
||||
throw new \Exception(sprintf('Error running command: composer require %s -v. Output: "%s". Error: "%s"', implode(' ', $dependencies), $composerProcess->getOutput(), $composerProcess->getErrorOutput()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->changeRootNamespaceIfNeeded();
|
||||
|
||||
file_put_contents($this->path.'/.gitignore', "var/cache/\nvendor/\n");
|
||||
|
||||
MakerTestProcess::create('git diff --quiet || ( git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git commit -a -m "second commit" )',
|
||||
$this->path
|
||||
)->run();
|
||||
} catch (\Exception $e) {
|
||||
$this->fs->remove($this->path);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
MakerTestProcess::create('git reset --hard && git clean -fd', $this->path)->run();
|
||||
$this->fs->remove($this->path.'/var/cache');
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand(string $command): MakerTestProcess
|
||||
{
|
||||
return MakerTestProcess::create($command, $this->path)->run();
|
||||
}
|
||||
|
||||
public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false, array $envVars = []): MakerTestProcess
|
||||
{
|
||||
// Let's remove cache
|
||||
$this->fs->remove($this->path.'/var/cache');
|
||||
|
||||
$testProcess = $this->createInteractiveCommandProcess(
|
||||
commandName: $this->testDetails->getMaker()::getCommandName(),
|
||||
userInputs: $inputs,
|
||||
argumentsString: $argumentsString,
|
||||
envVars: $envVars,
|
||||
);
|
||||
|
||||
$this->runnedMakerProcess = $testProcess->run($allowedToFail);
|
||||
|
||||
return $this->runnedMakerProcess;
|
||||
}
|
||||
|
||||
public function getGeneratedFilesFromOutputText(): array
|
||||
{
|
||||
$output = $this->runnedMakerProcess->getOutput();
|
||||
|
||||
$matches = [];
|
||||
|
||||
preg_match_all('#(created|updated): (]8;;[^]*\\\)?(.*?)(]8;;\\\)?\n#iu', $output, $matches, \PREG_PATTERN_ORDER);
|
||||
|
||||
return array_map('trim', $matches[3]);
|
||||
}
|
||||
|
||||
public function fileExists(string $file): bool
|
||||
{
|
||||
return $this->fs->exists($this->path.'/'.$file);
|
||||
}
|
||||
|
||||
public function runTwigCSLint(string $file): MakerTestProcess
|
||||
{
|
||||
if (!file_exists(__DIR__.'/../../tools/twigcs/vendor/bin/twigcs')) {
|
||||
throw new \Exception('twigcs not found: run: "composer install --working-dir=tools/twigcs".');
|
||||
}
|
||||
|
||||
return MakerTestProcess::create(sprintf('php tools/twigcs/vendor/bin/twigcs --config ./tools/twigcs/.twig_cs.dist %s', $this->path.'/'.$file), $this->rootPath)
|
||||
->run(true);
|
||||
}
|
||||
|
||||
private function buildFlexSkeleton(): void
|
||||
{
|
||||
$targetVersion = $this->getTargetSkeletonVersion();
|
||||
$versionString = $targetVersion ? sprintf(':%s', $targetVersion) : '';
|
||||
|
||||
$flexProjectDir = sprintf('flex_project%s', $targetVersion);
|
||||
|
||||
MakerTestProcess::create(
|
||||
sprintf('composer create-project symfony/skeleton%s %s --prefer-dist --no-progress', $versionString, $flexProjectDir),
|
||||
$this->cachePath
|
||||
)->run();
|
||||
|
||||
$rootPath = str_replace('\\', '\\\\', realpath(__DIR__.'/../..'));
|
||||
|
||||
$this->addMakerBundleRepoToComposer(sprintf('%s/%s/composer.json', $this->cachePath, $flexProjectDir));
|
||||
|
||||
// In Linux, git plays well with symlinks - we can add maker to the flex skeleton.
|
||||
if (!$this->isWindows) {
|
||||
$this->composerRequireMakerBundle(sprintf('%s/%s', $this->cachePath, $flexProjectDir));
|
||||
}
|
||||
|
||||
if ($_SERVER['MAKER_ALLOW_DEV_DEPS_IN_APP'] ?? false) {
|
||||
MakerTestProcess::create('composer config minimum-stability dev', $this->flexPath)->run();
|
||||
MakerTestProcess::create('composer config prefer-stable true', $this->flexPath)->run();
|
||||
}
|
||||
|
||||
// fetch a few packages needed for testing
|
||||
MakerTestProcess::create('composer require phpunit browser-kit symfony/css-selector --prefer-dist --no-progress --no-suggest', $this->flexPath)
|
||||
->run();
|
||||
|
||||
if ('\\' !== \DIRECTORY_SEPARATOR) {
|
||||
$this->fs->remove($this->flexPath.'/vendor/symfony/phpunit-bridge');
|
||||
|
||||
$this->fs->symlink($rootPath.'/vendor/symfony/phpunit-bridge', $this->flexPath.'/vendor/symfony/phpunit-bridge');
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
// temporarily ignoring indirect deprecations - see #237
|
||||
[
|
||||
'filename' => '.env.test',
|
||||
'find' => 'SYMFONY_DEPRECATIONS_HELPER=999999',
|
||||
'replace' => 'SYMFONY_DEPRECATIONS_HELPER=max[self]=0',
|
||||
],
|
||||
// do not explicitly set the PHPUnit version
|
||||
[
|
||||
'filename' => 'phpunit.xml.dist',
|
||||
'find' => '<server name="SYMFONY_PHPUNIT_VERSION" value="9.6" />',
|
||||
'replace' => '',
|
||||
],
|
||||
];
|
||||
$this->processReplacements($replacements, $this->flexPath);
|
||||
// end of temp code
|
||||
|
||||
file_put_contents($this->flexPath.'/.gitignore', "var/cache/\n");
|
||||
|
||||
// Force adding vendor/ dir to Git repo in case users exclude it in global .gitignore
|
||||
MakerTestProcess::create('git init && git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git add vendor/ -f && git commit -a -m "first commit"',
|
||||
$this->flexPath
|
||||
)->run();
|
||||
}
|
||||
|
||||
private function processReplacements(array $replacements, string $rootDir): void
|
||||
{
|
||||
foreach ($replacements as $replacement) {
|
||||
$this->processReplacement($rootDir, $replacement['filename'], $replacement['find'], $replacement['replace']);
|
||||
}
|
||||
}
|
||||
|
||||
public function processReplacement(string $rootDir, string $filename, string $find, string $replace, bool $allowNotFound = false): void
|
||||
{
|
||||
$path = realpath($rootDir.'/'.$filename);
|
||||
|
||||
if (!$this->fs->exists($path)) {
|
||||
if ($allowNotFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Could not find file "%s" to process replacements inside "%s"', $filename, $rootDir));
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
if (!str_contains($contents, $find)) {
|
||||
if ($allowNotFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Could not find "%s" inside "%s"', $find, $filename));
|
||||
}
|
||||
|
||||
file_put_contents($path, str_replace($find, $replace, $contents));
|
||||
}
|
||||
|
||||
public function createInteractiveCommandProcess(string $commandName, array $userInputs, string $argumentsString = '', array $envVars = []): MakerTestProcess
|
||||
{
|
||||
$envVars = array_merge(['SHELL_INTERACTIVE' => '1'], $envVars);
|
||||
|
||||
// We don't need ansi coloring in tests!
|
||||
$process = MakerTestProcess::create(
|
||||
commandLine: sprintf('php bin/console %s %s --no-ansi', $commandName, $argumentsString),
|
||||
cwd: $this->path,
|
||||
envVars: $envVars,
|
||||
timeout: 10
|
||||
);
|
||||
|
||||
if ($userInputs) {
|
||||
$inputStream = new InputStream();
|
||||
|
||||
// start the command with some input
|
||||
$inputStream->write(current($userInputs)."\n");
|
||||
|
||||
$inputStream->onEmpty(function () use ($inputStream, &$userInputs) {
|
||||
$nextInput = next($userInputs);
|
||||
if (false === $nextInput) {
|
||||
$inputStream->close();
|
||||
} else {
|
||||
$inputStream->write($nextInput."\n");
|
||||
}
|
||||
});
|
||||
|
||||
$process->setInput($inputStream);
|
||||
}
|
||||
|
||||
return $process;
|
||||
}
|
||||
|
||||
public function getSymfonyVersionInApp(): int
|
||||
{
|
||||
$contents = file_get_contents($this->getPath().'/vendor/symfony/http-kernel/Kernel.php');
|
||||
$position = strpos($contents, 'VERSION_ID = ');
|
||||
|
||||
return (int) substr($contents, $position + 13, 5);
|
||||
}
|
||||
|
||||
public function doesClassExistInApp(string $class): bool
|
||||
{
|
||||
$classMap = require $this->getPath().'/vendor/composer/autoload_classmap.php';
|
||||
|
||||
return isset($classMap[$class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the DependencyBuilder for the Maker command inside the
|
||||
* actual project, so we know exactly what dependencies we need or
|
||||
* don't need.
|
||||
*/
|
||||
private function determineMissingDependencies(): array
|
||||
{
|
||||
$depBuilder = $this->testDetails->getDependencyBuilder();
|
||||
file_put_contents($this->path.'/dep_builder', serialize($depBuilder));
|
||||
file_put_contents($this->path.'/dep_runner.php', '<?php
|
||||
|
||||
require __DIR__."/vendor/autoload.php";
|
||||
$depBuilder = unserialize(file_get_contents("dep_builder"));
|
||||
$missingDependencies = array_merge(
|
||||
$depBuilder->getMissingDependencies(),
|
||||
$depBuilder->getMissingDevDependencies()
|
||||
);
|
||||
echo json_encode($missingDependencies);
|
||||
');
|
||||
|
||||
$process = MakerTestProcess::create('php dep_runner.php', $this->path)->run();
|
||||
$data = json_decode($process->getOutput(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
unlink($this->path.'/dep_builder');
|
||||
unlink($this->path.'/dep_runner.php');
|
||||
|
||||
return array_merge($data, $this->testDetails->getExtraDependencies());
|
||||
}
|
||||
|
||||
public function getTargetSkeletonVersion(): ?string
|
||||
{
|
||||
return $_SERVER['SYMFONY_VERSION'] ?? '';
|
||||
}
|
||||
|
||||
private function composerRequireMakerBundle(string $projectDirectory): void
|
||||
{
|
||||
MakerTestProcess::create('composer require --dev symfony/maker-bundle', $projectDirectory)
|
||||
->run()
|
||||
;
|
||||
|
||||
$makerRepoSrcPath = sprintf('%s/maker-repo/src', $this->cachePath);
|
||||
|
||||
// DX - So we can test local changes without having to commit them.
|
||||
if (!is_link($makerRepoSrcPath)) {
|
||||
$this->fs->remove($makerRepoSrcPath);
|
||||
$this->fs->symlink(sprintf('%s/src', $this->rootPath), $makerRepoSrcPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Symfony/MakerBundle as a "path" repository to composer.json.
|
||||
*/
|
||||
private function addMakerBundleRepoToComposer(string $composerJsonPath): void
|
||||
{
|
||||
$composerJson = json_decode(
|
||||
file_get_contents($composerJsonPath), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
// Require-dev is empty and composer complains about this being an array when we encode it again.
|
||||
unset($composerJson['require-dev']);
|
||||
|
||||
$composerJson['repositories']['symfony/maker-bundle'] = [
|
||||
'type' => 'path',
|
||||
'url' => sprintf('%s%smaker-repo', $this->cachePath, \DIRECTORY_SEPARATOR),
|
||||
'options' => [
|
||||
'versions' => [
|
||||
'symfony/maker-bundle' => '9999.99', // Arbitrary version to avoid stability conflicts
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
file_put_contents($composerJsonPath, json_encode($composerJson, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
private function patchDoctrineDbalForSymfony7(): void
|
||||
{
|
||||
$commandPath = $this->path.'/vendor/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php';
|
||||
$contents = file_get_contents($commandPath);
|
||||
|
||||
$needle = 'protected function execute(InputInterface $input, OutputInterface $output)';
|
||||
if (str_contains($contents, $needle.': int')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contents = str_replace(
|
||||
$needle,
|
||||
$needle.': int',
|
||||
$contents
|
||||
);
|
||||
file_put_contents($commandPath, $contents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass;
|
||||
use Symfony\Bundle\MakerBundle\MakerBundle;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Kernel;
|
||||
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
|
||||
|
||||
class MakerTestKernel extends Kernel implements CompilerPassInterface
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
private string $testRootDir;
|
||||
|
||||
public function __construct(string $environment, bool $debug)
|
||||
{
|
||||
$this->testRootDir = sys_get_temp_dir().'/'.uniqid('sf_maker_', true);
|
||||
|
||||
parent::__construct($environment, $debug);
|
||||
}
|
||||
|
||||
public function registerBundles(): iterable
|
||||
{
|
||||
return [
|
||||
new FrameworkBundle(),
|
||||
new MakerBundle(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function configureRoutes(RoutingConfigurator $routes)
|
||||
{
|
||||
}
|
||||
|
||||
protected function configureRouting(RoutingConfigurator $routes)
|
||||
{
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
|
||||
{
|
||||
$c->loadFromExtension('framework', [
|
||||
'secret' => 123,
|
||||
'router' => [
|
||||
'utf8' => true,
|
||||
],
|
||||
'http_method_override' => false,
|
||||
'handle_all_throwables' => true,
|
||||
'php_errors' => [
|
||||
'log' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProjectDir(): string
|
||||
{
|
||||
return $this->getRootDir();
|
||||
}
|
||||
|
||||
public function getRootDir(): string
|
||||
{
|
||||
return $this->testRootDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// makes all makers public to help the tests
|
||||
foreach ($container->findTaggedServiceIds(MakeCommandRegistrationPass::MAKER_TAG) as $id => $tags) {
|
||||
$defn = $container->getDefinition($id);
|
||||
$defn->setPublic(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* @author Sadicov Vladimir <sadikoff@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class MakerTestProcess
|
||||
{
|
||||
private Process $process;
|
||||
|
||||
private function __construct($commandLine, $cwd, array $envVars, $timeout)
|
||||
{
|
||||
$this->process = \is_string($commandLine)
|
||||
? Process::fromShellCommandline($commandLine, $cwd, null, null, $timeout)
|
||||
: new Process($commandLine, $cwd, null, null, $timeout);
|
||||
|
||||
$this->process->setEnv($envVars);
|
||||
}
|
||||
|
||||
public static function create($commandLine, $cwd, array $envVars = [], $timeout = null): self
|
||||
{
|
||||
return new self($commandLine, $cwd, $envVars, $timeout);
|
||||
}
|
||||
|
||||
public function setInput($input): self
|
||||
{
|
||||
$this->process->setInput($input);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run($allowToFail = false, array $envVars = []): self
|
||||
{
|
||||
$this->process->run(null, $envVars);
|
||||
|
||||
if (!$allowToFail && !$this->process->isSuccessful()) {
|
||||
throw new \Exception(sprintf('Error running command: "%s". Output: "%s". Error: "%s"', $this->process->getCommandLine(), $this->process->getOutput(), $this->process->getErrorOutput()));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isSuccessful(): bool
|
||||
{
|
||||
return $this->process->isSuccessful();
|
||||
}
|
||||
|
||||
public function getOutput(): string
|
||||
{
|
||||
return $this->process->getOutput();
|
||||
}
|
||||
|
||||
public function getErrorOutput(): string
|
||||
{
|
||||
return $this->process->getErrorOutput();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony MakerBundle package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\MakerBundle\Test;
|
||||
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
|
||||
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
class MakerTestRunner
|
||||
{
|
||||
private Filesystem $filesystem;
|
||||
private ?MakerTestProcess $executedMakerProcess = null;
|
||||
|
||||
public function __construct(
|
||||
private MakerTestEnvironment $environment,
|
||||
) {
|
||||
$this->filesystem = new Filesystem();
|
||||
}
|
||||
|
||||
public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false, array $envVars = []): string
|
||||
{
|
||||
$this->executedMakerProcess = $this->environment->runMaker($inputs, $argumentsString, $allowedToFail, $envVars);
|
||||
|
||||
return $this->executedMakerProcess->getOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function copy(string $source, string $destination)
|
||||
{
|
||||
$path = __DIR__.'/../../tests/fixtures/'.$source;
|
||||
|
||||
if (!file_exists($path)) {
|
||||
throw new \Exception(sprintf('Cannot find file "%s"', $path));
|
||||
}
|
||||
|
||||
if (is_file($path)) {
|
||||
$this->filesystem->copy($path, $this->getPath($destination), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// handle a directory copy
|
||||
$finder = new Finder();
|
||||
$finder->in($path)->files();
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$this->filesystem->copy($file->getPathname(), $this->getPath($file->getRelativePathname()), true);
|
||||
}
|
||||
}
|
||||
|
||||
public function renderTemplateFile(string $source, string $destination, array $variables): void
|
||||
{
|
||||
$twig = new Environment(
|
||||
new FilesystemLoader(__DIR__.'/../../tests/fixtures')
|
||||
);
|
||||
|
||||
$rendered = $twig->render($source, $variables);
|
||||
|
||||
$this->filesystem->mkdir(\dirname($this->getPath($destination)));
|
||||
file_put_contents($this->getPath($destination), $rendered);
|
||||
}
|
||||
|
||||
public function getPath(string $filename): string
|
||||
{
|
||||
return $this->environment->getPath().'/'.$filename;
|
||||
}
|
||||
|
||||
public function readYaml(string $filename): array
|
||||
{
|
||||
return Yaml::parse(file_get_contents($this->getPath($filename)));
|
||||
}
|
||||
|
||||
public function getExecutedMakerProcess(): MakerTestProcess
|
||||
{
|
||||
if (!$this->executedMakerProcess) {
|
||||
throw new \Exception('Maker process has not been executed yet.');
|
||||
}
|
||||
|
||||
return $this->executedMakerProcess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function modifyYamlFile(string $filename, \Closure $callback)
|
||||
{
|
||||
$path = $this->getPath($filename);
|
||||
$manipulator = new YamlSourceManipulator(file_get_contents($path));
|
||||
|
||||
$newData = $callback($manipulator->getData());
|
||||
if (!\is_array($newData)) {
|
||||
throw new \Exception('The modifyYamlFile() callback must return the final array of data');
|
||||
}
|
||||
$manipulator->setData($newData);
|
||||
|
||||
file_put_contents($path, $manipulator->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function runConsole(string $command, array $inputs, string $arguments = '')
|
||||
{
|
||||
$process = $this->environment->createInteractiveCommandProcess(
|
||||
$command,
|
||||
$inputs,
|
||||
$arguments
|
||||
);
|
||||
|
||||
$process->run();
|
||||
}
|
||||
|
||||
public function runProcess(string $command): void
|
||||
{
|
||||
MakerTestProcess::create($command, $this->environment->getPath())->run();
|
||||
}
|
||||
|
||||
public function replaceInFile(string $filename, string $find, string $replace, bool $allowNotFound = false): void
|
||||
{
|
||||
$this->environment->processReplacement(
|
||||
$this->environment->getPath(),
|
||||
$filename,
|
||||
$find,
|
||||
$replace,
|
||||
$allowNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public function removeFromFile(string $filename, string $find, bool $allowNotFound = false): void
|
||||
{
|
||||
$this->environment->processReplacement(
|
||||
$this->environment->getPath(),
|
||||
$filename,
|
||||
$find,
|
||||
'',
|
||||
$allowNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public function configureDatabase(bool $createSchema = true): void
|
||||
{
|
||||
$this->replaceInFile(
|
||||
'.env',
|
||||
'postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8',
|
||||
getenv('TEST_DATABASE_DSN')
|
||||
);
|
||||
|
||||
// Flex includes a recipe to suffix the dbname w/ "_test" - lets keep
|
||||
// things simple for these tests and not do that.
|
||||
$this->modifyYamlFile('config/packages/doctrine.yaml', function (array $config) {
|
||||
if (isset($config['when@test']['doctrine']['dbal']['dbname_suffix'])) {
|
||||
unset($config['when@test']['doctrine']['dbal']['dbname_suffix']);
|
||||
}
|
||||
|
||||
return $config;
|
||||
});
|
||||
|
||||
// this looks silly, but it's the only way to drop the database *for sure*,
|
||||
// as doctrine:database:drop will error if there is no database
|
||||
// also, skip for SQLITE, as it does not support --if-not-exists
|
||||
if (!str_starts_with(getenv('TEST_DATABASE_DSN'), 'sqlite://')) {
|
||||
$this->runConsole('doctrine:database:create', [], '--env=test --if-not-exists');
|
||||
}
|
||||
$this->runConsole('doctrine:database:drop', [], '--env=test --force');
|
||||
|
||||
$this->runConsole('doctrine:database:create', [], '--env=test');
|
||||
if ($createSchema) {
|
||||
$this->runConsole('doctrine:schema:create', [], '--env=test');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateSchema(): void
|
||||
{
|
||||
$this->runConsole('doctrine:schema:update', [], '--env=test --force');
|
||||
}
|
||||
|
||||
public function runTests(): void
|
||||
{
|
||||
$internalTestProcess = MakerTestProcess::create(
|
||||
sprintf('php %s', $this->getPath('/bin/phpunit')),
|
||||
$this->environment->getPath())
|
||||
->run(true)
|
||||
;
|
||||
|
||||
if ($internalTestProcess->isSuccessful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ExpectationFailedException(sprintf("Error while running the PHPUnit tests *in* the project: \n\n %s \n\n Command Output: %s", $internalTestProcess->getErrorOutput()."\n".$internalTestProcess->getOutput(), $this->getExecutedMakerProcess()->getErrorOutput()."\n".$this->getExecutedMakerProcess()->getOutput()));
|
||||
}
|
||||
|
||||
public function writeFile(string $filename, string $contents): void
|
||||
{
|
||||
$this->filesystem->mkdir(\dirname($this->getPath($filename)));
|
||||
file_put_contents($this->getPath($filename), $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addToAutoloader(string $namespace, string $path)
|
||||
{
|
||||
$composerJson = json_decode(
|
||||
json: file_get_contents($this->getPath('composer.json')),
|
||||
associative: true,
|
||||
flags: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
|
||||
$composerJson['autoload-dev']['psr-4'][$namespace] = $path;
|
||||
|
||||
$this->filesystem->dumpFile(
|
||||
$this->getPath('composer.json'),
|
||||
json_encode($composerJson, \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->environment->runCommand('composer dump-autoload');
|
||||
}
|
||||
|
||||
public function deleteFile(string $filename): void
|
||||
{
|
||||
$this->filesystem->remove($this->getPath($filename));
|
||||
}
|
||||
|
||||
public function manipulateClass(string $filename, \Closure $callback): void
|
||||
{
|
||||
$contents = file_get_contents($this->getPath($filename));
|
||||
$manipulator = new ClassSourceManipulator(
|
||||
sourceCode: $contents,
|
||||
overwrite: true,
|
||||
);
|
||||
$callback($manipulator);
|
||||
|
||||
file_put_contents($this->getPath($filename), $manipulator->getSourceCode());
|
||||
}
|
||||
|
||||
public function getSymfonyVersion(): int
|
||||
{
|
||||
return $this->environment->getSymfonyVersionInApp();
|
||||
}
|
||||
|
||||
public function doesClassExist(string $class): bool
|
||||
{
|
||||
return $this->environment->doesClassExistInApp($class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user