Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Cake\Console\CommandCollection;
use Cake\Core\ConsoleApplicationInterface;
use Cake\Upgrade\Command\FileRenameCommand;
use Cake\Upgrade\Command\LinterCommand;
use Cake\Upgrade\Command\RectorCommand;
use Cake\Upgrade\Command\UpgradeCommand;

Expand Down Expand Up @@ -51,6 +52,7 @@ public function console(CommandCollection $commands): CommandCollection
$commands->add('upgrade', UpgradeCommand::class);
$commands->add('upgrade file_rename', FileRenameCommand::class);
$commands->add('upgrade rector', RectorCommand::class);
$commands->add('linter', LinterCommand::class);

return $commands;
}
Expand Down
163 changes: 163 additions & 0 deletions src/Command/Helper/ProgressHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP Project
* @since 3.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Upgrade\Command\Helper;

use Cake\Console\Helper;
use InvalidArgumentException;

/**
* Create a progress bar using a supplied callback.
*
* ## Usage
*
* The ProgressHelper can be accessed from shells using the helper() method
*
* ```
* $this->helper('Progress')->output(['callback' => function ($progress) {
* // Do work
* $progress->increment();
* });
* ```
*/
class ProgressHelper extends Helper
{
/**
* Default value for progress bar total value.
* Percent completion is derived from progress/total
*/
protected const DEFAULT_TOTAL = 100;

/**
* Default value for progress bar width
*/
protected const DEFAULT_WIDTH = 80;

/**
* The current progress.
*
* @var float|int
*/
protected float|int $_progress = 0;

/**
* The total number of 'items' to progress through.
*
* @var int
*/
protected int $_total = self::DEFAULT_TOTAL;

/**
* The width of the bar.
*
* @var int
*/
protected int $_width = self::DEFAULT_WIDTH;

/**
* Output a progress bar.
*
* Takes a number of options to customize the behavior:
*
* - `total` The total number of items in the progress bar. Defaults
* to 100.
* - `width` The width of the progress bar. Defaults to 80.
* - `callback` The callback that will be called in a loop to advance the progress bar.
*
* @param array $args The arguments/options to use when outputting the progress bar.
* @return void
*/
public function output(array $args): void
{
$args += ['callback' => null];
if (isset($args[0])) {
$args['callback'] = $args[0];
}
if (!$args['callback'] || !is_callable($args['callback'])) {
throw new InvalidArgumentException('Callback option must be a callable.');
}
$this->init($args);

$callback = $args['callback'];

$this->_io->out('', 0);
while ($this->_progress < $this->_total) {
$callback($this);
$this->draw();
}
$this->_io->out('');
}

/**
* Initialize the progress bar for use.
*
* - `total` The total number of items in the progress bar. Defaults
* to 100.
* - `width` The width of the progress bar. Defaults to 80.
*
* @param array<string, mixed> $args The initialization data.
* @return $this
*/
public function init(array $args = [])
{
$args += ['total' => self::DEFAULT_TOTAL, 'width' => self::DEFAULT_WIDTH];
$this->_progress = 0;
$this->_width = $args['width'];
$this->_total = $args['total'];

return $this;
}

/**
* Increment the progress bar.
*
* @param float|int $num The amount of progress to advance by.
* @return $this
*/
public function increment(float|int $num = 1)
{
$this->_progress = min(max(0, $this->_progress + $num), $this->_total);

return $this;
}

/**
* Render the progress bar based on the current state.
*
* @return $this
*/
public function draw()
{
$numberLen = strlen(' 100%');
$complete = round($this->_progress / $this->_total, 2);
$barLen = ($this->_width - $numberLen) * $this->_progress / $this->_total;
$bar = '';
if ($barLen > 1) {
$bar = str_repeat('=', (int)$barLen - 1) . '>';
}

$pad = ceil($this->_width - $numberLen - $barLen);
if ($pad > 0) {
$bar .= str_repeat(' ', (int)$pad);
}
$percent = ($complete * 100) . '%';
$bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT);

$this->_io->overwrite($bar, 0);

return $this;
}
}
154 changes: 154 additions & 0 deletions src/Command/LinterCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);

namespace Cake\Upgrade\Command;

use Cake\Console\Arguments;
use Cake\Console\BaseCommand;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
use Symfony\Component\Process\Process;

class LinterCommand extends BaseCommand
{
protected static array $defaultDirectories = [
'src/',
'config/',
'templates/',
'tests/',
];

/**
* The name of this command.
*
* @var string
*/
protected string $name = 'linter';

/**
* Get the command description.
*
* @return string
*/
public static function getDescription(): string
{
return 'Check PHP files using PHP linter.';
}

/**
* Hook method for defining this command's option parser.
*
* @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined
* @return \Cake\Console\ConsoleOptionParser The built parser.
*/
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$dirs = [];
foreach ($this->defaultDirectories() as $dir) {
$dirs[] = '`' . $dir . '`';
}

$help = 'The path (or comma separated paths) to the file or directory to check.';
$help .= ' If not provided, defaults to checking common directories ' . implode(', ', $dirs) . '.';

return parent::buildOptionParser($parser)
->addArgument('path', [
'help' => $help,
'default' => null,
'required' => false,
])
->setDescription(static::getDescription());
}

/**
* Implement this method with your command's logic.
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
* @return int The exit code
*/
public function execute(Arguments $args, ConsoleIo $io): int
{
$directories = $args->getArgument('path') ?: $this->defaultDirectories();
if (is_string($directories) && str_contains($directories, ',')) {
$directories = explode(',', $directories);
}

$result = static::CODE_SUCCESS;
$errors = [];
foreach ((array)$directories as $directory) {
if (!file_exists($directory)) {
$io->warning('Does not exist: ' . $directory . ' - skipping.');

continue;
}

$io->out('Checking ' . (is_file($directory) ? 'file' : 'directory') . ': ' . $directory);
if (is_file($directory)) {
$phpFiles = [
new SplFileInfo($directory),
];
} else {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
$phpFiles = new RegexIterator($files, '/\.php$/');
}

$fileCount = iterator_count($phpFiles);
$io->helper('Progress')->init(['total' => $fileCount]);
$io->out('', 0);

/** @var \SplFileInfo $file */
foreach ($phpFiles as $file) {
$io->verbose('Checking ' . $file->getPathname());

$command = 'php -l ' . $file->getPathname();
$process = Process::fromShellCommandline($command);
$process->run();

if ($process->getExitCode() !== static::CODE_SUCCESS) {
$errors[] = $file->getPathname() . ': ' . $process->getErrorOutput();
$result = self::CODE_ERROR;

continue;
}

$io->helper('Progress')->increment();
$io->helper('Progress')->draw();
}

$io->out('');
}

if ($errors) {
$io->err($errors);
}

if ($result === self::CODE_SUCCESS) {
$io->success('All files are valid.');
} else {
$io->error('Some files have errors. Please check the output above.');
}

return $result;
}

/**
* @return array<string>
*/
protected function defaultDirectories(): array
{
$result = [];
foreach (static::$defaultDirectories as $directory) {
if (!is_dir($directory)) {
continue;
}
$result[] = $directory;
}

return $result;
}
}
61 changes: 61 additions & 0 deletions tests/TestCase/Command/LinterCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 6.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Upgrade\Test\TestCase\Command;

use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
use Cake\Upgrade\Test\TestCase\TestCase;

class LinterCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;

/**
* setup method
*
* @return void
*/
public function setUp(): void
{
parent::setUp();

$this->configApplication('\Cake\Upgrade\Application', []);
}

/**
* @return void
*/
public function testLinterSuccess()
{
$this->exec('linter');

$this->assertExitSuccess();
$this->assertOutputContains('All files are valid.');
}

/**
* @return void
*/
public function testLinterFail()
{
mkdir(TMP . 'linter', 0777, true);
file_put_contents(TMP . 'linter/BrokenExample.php', '<?php class X }');
$this->exec('linter tmp/linter/ -v');

$this->assertExitError();
$this->assertErrorContains('Some files have errors.');
}
}
Loading