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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ Run the test suite:
./shift test
```

Run local quality checks:

```sh
./shift lint
./shift qa
```

`shift lint` checks PHP syntax and basic file hygiene. `shift qa` runs Composer validation, lint checks, the test suite, and route listing.

Run the example module command:

```sh
Expand Down
3 changes: 2 additions & 1 deletion REFACTORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled
- [x] Structured exception logging with JSON file logger and service container override.
- [x] Request id lifecycle with generated `X-Request-Id` response headers and log context.
- [x] Developer documentation organized into a Laravel-like guide.
- [x] CLI quality gate with `shift lint` and `shift qa`.
- [x] Removal of view storage and example page assets from runtime.
- [x] Removal of legacy `application/controllers` and `application/routes.php`.
- [x] Domain-oriented framework namespaces:
Expand Down Expand Up @@ -165,4 +166,4 @@ Internal errors return a generic `500` message unless `display_errors` is enable

## Next

- [ ] Static analysis and coding style checks.
- [ ] Deeper static analysis for framework contracts and type safety.
21 changes: 18 additions & 3 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ <h2>Operations</h2>
<ul>
<li><a href="#logging">Logging</a></li>
<li><a href="#cache">Cache</a></li>
<li><a href="#quality-checks">Quality Checks</a></li>
<li><a href="#doctor">Doctor</a></li>
<li><a href="#testing">Testing</a></li>
<li><a href="#releases">Releases</a></li>
Expand Down Expand Up @@ -325,7 +326,8 @@ <h2>Installation</h2>

<p>Run the framework checks:</p>

<pre><code>./shift doctor</code></pre>
<pre><code>./shift doctor
./shift qa</code></pre>
</section>

<section id="configuration">
Expand Down Expand Up @@ -806,13 +808,26 @@ <h2>Cache</h2>
<p>Rebuild the module cache after changing module boundaries, module config, or module command mappings.</p>
</section>

<section id="quality-checks">
<h2>Quality Checks</h2>
<p>The CLI includes zero-dependency quality gates for local development and pull request preparation.</p>

<pre><code>./shift lint
./shift qa</code></pre>

<p><code>shift lint</code> checks PHP syntax and basic file hygiene, including trailing whitespace and missing final newlines. <code>shift qa</code> runs Composer validation, lint checks, the test suite, and the route list command.</p>

<pre><code>./shift help lint
./shift help qa</code></pre>
</section>

<section id="doctor">
<h2>Doctor</h2>
<p>The doctor command runs local diagnostics and returns a non-zero exit code when a required check fails.</p>

<pre><code>./shift doctor</code></pre>

<p>It checks PHP version, required extensions, Composer JSON validity, PHP lint, the test suite, environment presence, database config, and module cache status.</p>
<p>It checks PHP version, required extensions, Composer JSON validity, PHP syntax, the test suite, environment presence, database config, and module cache status.</p>
</section>

<section id="testing">
Expand All @@ -822,7 +837,7 @@ <h2>Testing</h2>
<pre><code>composer test
./shift test</code></pre>

<p>The GitHub workflow validates Composer config, dumps autoload files, lints PHP files, runs tests, and verifies the route list command.</p>
<p>The GitHub workflow validates Composer config, dumps autoload files, lints PHP files, runs tests, and verifies the route list command. Locally, <code>./shift qa</code> runs the same kind of pre-PR quality gate.</p>
</section>

<section id="releases">
Expand Down
62 changes: 4 additions & 58 deletions src/Console/Commands/Doctor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Shift\Console\Cli;
use Shift\Console\CommandInterface;
use Shift\Console\Quality\QualityChecks;
use Shift\Database\DatabaseConfig;
use Shift\Modules\ModuleLoader;
use Throwable;
Expand All @@ -14,12 +15,13 @@ class Doctor implements CommandInterface
public function execute(mixed ...$args): void
{
$cli = new Cli();
$quality = new QualityChecks();
$checks = [
$this->phpVersion(),
$this->extensions(),
$this->composerConfig(),
$this->phpLint(),
$this->testSuite(),
$quality->phpSyntax()->toRow(),
$quality->testSuite()->toRow(),
$this->environment(),
$this->databaseConfig(),
$this->moduleCache(),
Expand Down Expand Up @@ -87,41 +89,6 @@ private function composerConfig(): array
];
}

private function phpLint(): array
{
$files = array_merge(
$this->phpFiles(APP_ROOT . '/src'),
$this->phpFiles(APP_ROOT . '/application'),
$this->phpFiles(APP_ROOT . '/tests'),
[APP_ROOT . '/shift']
);

foreach ($files as $file) {
if (!is_file($file)) {
continue;
}

exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $exitCode);

if ($exitCode !== 0) {
return ['PHP lint', 'fail', basename($file) . ': ' . trim(implode(' ', $output))];
}
}

return ['PHP lint', 'ok', count($files) . ' file(s) checked'];
}

private function testSuite(): array
{
exec('composer test 2>&1', $output, $exitCode);

return [
'Test suite',
$exitCode === 0 ? 'ok' : 'fail',
$exitCode === 0 ? 'composer test passed' : trim(implode(' ', array_slice($output, -3))),
];
}

private function environment(): array
{
return [
Expand Down Expand Up @@ -157,25 +124,4 @@ private function moduleCache(): array
];
}

private function phpFiles(string $path): array
{
if (!is_dir($path)) {
return [];
}

$files = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
);

foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}

sort($files);

return $files;
}
}
49 changes: 49 additions & 0 deletions src/Console/Commands/Lint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Console\Commands;

use Shift\Console\Cli;
use Shift\Console\CommandInterface;
use Shift\Console\Quality\CheckResult;
use Shift\Console\Quality\QualityChecks;

#[\Shift\Console\Attributes\Command('lint', aliases: ['l'], group: 'diagnostics')]
class Lint implements CommandInterface
{
public function execute(mixed ...$args): void
{
$this->render((new QualityChecks())->lint(), 'Lint checks passed.', 'Lint checks failed.');
}

public function getHelp(): string
{
return 'Usage: ./shift lint';
}

public function getDescription(): string
{
return 'Run PHP syntax and file hygiene checks.';
}

/**
* @param list<CheckResult> $results
*/
private function render(array $results, string $successMessage, string $failureMessage): void
{
$cli = new Cli();
$cli->table(['Check', 'Status', 'Details'], array_map(
static fn (CheckResult $result): array => $result->toRow(),
$results
));

$failed = array_values(array_filter($results, static fn (CheckResult $result): bool => !$result->passed()));

if ($failed === []) {
$cli->success($successMessage);
return;
}

$cli->error($failureMessage);
exit(1);
}
}
49 changes: 49 additions & 0 deletions src/Console/Commands/Qa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Console\Commands;

use Shift\Console\Cli;
use Shift\Console\CommandInterface;
use Shift\Console\Quality\CheckResult;
use Shift\Console\Quality\QualityChecks;

#[\Shift\Console\Attributes\Command('qa', aliases: ['quality', 'ci'], group: 'diagnostics')]
class Qa implements CommandInterface
{
public function execute(mixed ...$args): void
{
$this->render((new QualityChecks())->qa(), 'Quality checks passed.', 'Quality checks failed.');
}

public function getHelp(): string
{
return 'Usage: ./shift qa';
}

public function getDescription(): string
{
return 'Run Composer validation, lint, tests, and route checks.';
}

/**
* @param list<CheckResult> $results
*/
private function render(array $results, string $successMessage, string $failureMessage): void
{
$cli = new Cli();
$cli->table(['Check', 'Status', 'Details'], array_map(
static fn (CheckResult $result): array => $result->toRow(),
$results
));

$failed = array_values(array_filter($results, static fn (CheckResult $result): bool => !$result->passed()));

if ($failed === []) {
$cli->success($successMessage);
return;
}

$cli->error($failureMessage);
exit(1);
}
}
4 changes: 2 additions & 2 deletions src/Console/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Console
{
/**
* Instancja klasy Cli do operacji terminalowych
*
*
* @var Cli
*/
private Cli $cli;
Expand All @@ -31,7 +31,7 @@ public function __construct()

/**
* Zwraca instancję klasy Cli
*
*
* @return Cli
*/
public function cli(): Cli
Expand Down
36 changes: 36 additions & 0 deletions src/Console/Quality/CheckResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Shift\Console\Quality;

final class CheckResult
{
public function __construct(
public readonly string $name,
public readonly string $status,
public readonly string $details
) {
}

public static function ok(string $name, string $details): self
{
return new self($name, 'ok', $details);
}

public static function fail(string $name, string $details): self
{
return new self($name, 'fail', $details);
}

/**
* @return array{0: string, 1: string, 2: string}
*/
public function toRow(): array
{
return [$this->name, $this->status, $this->details];
}

public function passed(): bool
{
return $this->status !== 'fail';
}
}
Loading
Loading