diff --git a/.editorconfig b/.editorconfig index ac7653b..5e5b915 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,16 +7,10 @@ charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true - -[*.{md,html}] indent_style = tab indent_size = tab tab_width = 4 -[*.{js,ts}] -indent_style = space -indent_size = 2 - -[{*.json,.travis.yml}] +[*.{json,yaml,yml,md}] indent_style = space indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..aad8529 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +.docs export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +Makefile export-ignore +README.md export-ignore +phpstan.neon export-ignore +ruleset.xml export-ignore +tests export-ignore diff --git a/.github/workflows/codesniffer.yml b/.github/workflows/codesniffer.yml new file mode 100644 index 0000000..a58ac4f --- /dev/null +++ b/.github/workflows/codesniffer.yml @@ -0,0 +1,18 @@ +name: "Codesniffer" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 8 * * 1" + +jobs: + codesniffer: + name: "Codesniffer" + uses: contributte/.github/.github/workflows/codesniffer.yml@master + with: + php: "8.2" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..fac01f8 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,18 @@ +name: "Coverage" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 9 * * 1" + +jobs: + coverage: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester-coverage-v2.yml@master + with: + php: "8.2" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..13ceb07 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,18 @@ +name: "Phpstan" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 10 * * 1" + +jobs: + phpstan: + name: "Phpstan" + uses: contributte/.github/.github/workflows/phpstan.yml@master + with: + php: "8.2" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3bee0e7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: "Nette Tester" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 10 * * 1" + +jobs: + test85: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.5" + + test84: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.4" + + test83: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.3" + + test82: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.2" + composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest" diff --git a/.gitignore b/.gitignore index e4b95ba..f0b3670 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,11 @@ # Composer /vendor /composer.lock + +# Tests +/tests/tmp +/coverage.* +/tests/**/*.log +/tests/**/*.html +/tests/**/*.expected +/tests/**/*.actual diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..33bc117 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +.PHONY: install +install: + composer update + +.PHONY: qa +qa: phpstan cs + +.PHONY: cs +cs: +ifdef GITHUB_ACTION + vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp -q --report=checkstyle src tests | cs2pr +else + vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp src tests +endif + +.PHONY: csf +csf: + vendor/bin/phpcbf --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp src tests + +.PHONY: phpstan +phpstan: + vendor/bin/phpstan analyse -c phpstan.neon + +.PHONY: tests +tests: + vendor/bin/tester -s -p php --colors 1 -C tests/Cases + +.PHONY: coverage +coverage: +ifdef GITHUB_ACTION + vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.xml --coverage-src src tests/Cases +else + vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.html --coverage-src src tests/Cases +endif diff --git a/composer.json b/composer.json index c2c3a4b..f5955c9 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "lab/dag", - "description": ":running: Dag is opinionated expression data generator written in PHP to fake your API.", + "description": "Dag is opinionated expression data generator written in PHP to fake your API.", "keywords": [ "php", "api", @@ -18,27 +18,36 @@ } ], "require": { - "php": "^7.3", - "contributte/http": "^0.3.0", - "contributte/utils": "^0.4.1", - "contributte/di": "^0.4.0", - "contributte/tracy": "^0.4.1", - "nette/routing": "^3.0.0", - "fzaninotto/faker": "^1.9.0", - "nelmio/alice": "^3.5.7" + "php": ">=8.2", + "contributte/di": "~0.6.0", + "contributte/http": "~0.5.0", + "contributte/tracy": "~0.7.0", + "contributte/utils": "~0.7.0", + "nelmio/alice": "^3.12.0", + "nette/routing": "^3.0.0" }, "require-dev": { - "contributte/dev": "^0.1.0" + "contributte/phpstan": "~0.2.0", + "contributte/qa": "~0.4.0", + "contributte/tester": "~0.3.0" }, "autoload": { "psr-4": { "Dag\\": "src" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, "minimum-stability": "dev", "prefer-stable": true, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "extra": { "branch-alias": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..bee6315 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,127 @@ +parameters: + ignoreErrors: + - + message: '#^Class Nette\\DI\\Statement does not have a constructor and must be instantiated without any parameters\.$#' + identifier: new.noConstructor + count: 3 + path: src/DI/DagExtension.php + + - + message: ''' + #^Instantiation of deprecated class Nette\\DI\\Statement\: + use Nette\\DI\\Definitions\\Statement$# + ''' + identifier: new.deprecatedClass + count: 3 + path: src/DI/DagExtension.php + + - + message: '#^Parameter \#1 \$factory of method Nette\\DI\\Definitions\\ServiceDefinition\:\:setFactory\(\) expects array\|Nette\\DI\\Definitions\\Definition\|Nette\\DI\\Definitions\\Reference\|Nette\\DI\\Definitions\\Statement\|string, Nette\\DI\\Statement given\.$#' + identifier: argument.type + count: 1 + path: src/DI/DagExtension.php + + - + message: '#^Binary operation "\." between mixed and ''/container'' results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Dag.php + + - + message: '#^Call to an undefined method object\:\:initialize\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Dag.php + + - + message: ''' + #^Fetching deprecated class constant DETECT of class Tracy\\Debugger\: + use Debugger\:\:Detect$# + ''' + identifier: classConstant.deprecated + count: 1 + path: src/Dag.php + + - + message: '#^Method Dag\\Dag\:\:createContainer\(\) should return Nette\\DI\\Container but returns object\.$#' + identifier: return.type + count: 1 + path: src/Dag.php + + - + message: '#^Only booleans are allowed in a ternary operator condition, mixed given\.$#' + identifier: ternary.condNotBoolean + count: 1 + path: src/Dag.php + + - + message: '#^Parameter \#1 \$file of method Nette\\DI\\Compiler\:\:loadConfig\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Dag.php + + - + message: '#^Parameter \#2 \$autoRebuild of class Nette\\DI\\ContainerLoader constructor expects bool, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Dag.php + + - + message: '#^Method Dag\\Generator\\Impl\\AliceGenerator\:\:generate\(\) has parameter \$input with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Generator/Impl/AliceGenerator.php + + - + message: '#^Parameter \#1 \$input \(Throwable\) of method Dag\\Generator\\Impl\\ExceptionGenerator\:\:generate\(\) should be contravariant with parameter \$input \(mixed\) of method Dag\\Generator\\Impl\\IGenerator\:\:generate\(\)$#' + identifier: method.childParameterType + count: 1 + path: src/Generator/Impl/ExceptionGenerator.php + + - + message: '#^Method Dag\\Generator\\Impl\\FallbackGenerator\:\:generate\(\) has parameter \$input with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Generator/Impl/FallbackGenerator.php + + - + message: '#^Only booleans are allowed in a negated boolean, string\|null given\.$#' + identifier: booleanNot.exprNotBoolean + count: 1 + path: src/Grub.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: src/Grub.php + + - + message: '#^Parameter \#1 \$name of method Dag\\Generator\\Generator\:\:generate\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Handler/Executor.php + + - + message: '#^Parameter \#2 \$value of method Nette\\Http\\Response\:\:setHeader\(\) expects string\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Http/DagResponse.php + + - + message: '#^Method Dag\\Router\\Router\:\:match\(\) should return array\\|null but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Router/Router.php + + - + message: '#^Method Dag\\Utils\\Arrays\:\:toArray\(\) should return array\ but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Utils/Arrays.php + + - + message: '#^Parameter \#1 \$json of function json_decode expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: src/Utils/Arrays.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..fb78f55 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/contributte/phpstan/phpstan.neon + - phpstan-baseline.neon + +parameters: + level: 9 + phpVersion: 80200 + + scanDirectories: + - src + + fileExtensions: + - php + + paths: + - src + - .docs diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..b48ea3f --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + /tests/tmp + diff --git a/src/DI/DagExtension.php b/src/DI/DagExtension.php index c6868a6..8deff0d 100644 --- a/src/DI/DagExtension.php +++ b/src/DI/DagExtension.php @@ -41,7 +41,7 @@ public function getConfigSchema(): Schema ]); } - public function loadConfiguration() + public function loadConfiguration(): void { $builder = $this->getContainerBuilder(); $config = (array) $this->config; diff --git a/src/Dag.php b/src/Dag.php index c4eabc1..bb1ed95 100644 --- a/src/Dag.php +++ b/src/Dag.php @@ -15,21 +15,24 @@ class Dag { /** @var mixed[] */ - private $config; + private array $config; + /** + * @param mixed[] $config + */ public function __construct(array $config) { $this->config = $config; } - public function trace() + public function trace(): void { // Error tracking Debugger::$strictMode = true; Debugger::enable($this->config['debug'] ? Debugger::DEBUG : Debugger::DETECT, $this->config['logDir']); } - public function serve() + public function serve(): void { $container = $this->createContainer(); @@ -51,6 +54,7 @@ private function createContainer(): Container $class = $this->loadContainer(); $container = new $class(); $container->initialize(); + return $container; } @@ -60,15 +64,14 @@ private function loadContainer(): string $this->config['tempDir'] . '/container', $this->config['debug'] ); - $class = $loader->load( - function (Compiler $compiler) { + + return $loader->load( + function (Compiler $compiler): void { $compiler->addExtension('dag', new DagExtension()); $compiler->loadConfig($this->config['config']); }, [$this->config, PHP_VERSION_ID - PHP_RELEASE_VERSION] ); - - return $class; } } diff --git a/src/Generator/Generator.php b/src/Generator/Generator.php index 52aef57..27450f8 100644 --- a/src/Generator/Generator.php +++ b/src/Generator/Generator.php @@ -8,7 +8,7 @@ final class Generator { /** @var IGenerator[] */ - private $generators = []; + private array $generators = []; public function add(string $name, IGenerator $generator): void { @@ -18,7 +18,7 @@ public function add(string $name, IGenerator $generator): void /** * @return mixed[] */ - public function generate(string $name, $content): array + public function generate(string $name, mixed $content): array { return $this->generators[$name]->generate($content); } diff --git a/src/Generator/Impl/AliceGenerator.php b/src/Generator/Impl/AliceGenerator.php index 3e29ea5..3e66822 100644 --- a/src/Generator/Impl/AliceGenerator.php +++ b/src/Generator/Impl/AliceGenerator.php @@ -8,8 +8,7 @@ final class AliceGenerator implements IGenerator { - /** @var NativeLoader */ - private $loader; + private NativeLoader $loader; public function __construct(NativeLoader $loader) { diff --git a/src/Generator/Impl/ExceptionGenerator.php b/src/Generator/Impl/ExceptionGenerator.php index c8007eb..683f7fa 100644 --- a/src/Generator/Impl/ExceptionGenerator.php +++ b/src/Generator/Impl/ExceptionGenerator.php @@ -8,13 +8,13 @@ final class ExceptionGenerator implements IGenerator { /** - * @param Throwable $e + * @param Throwable $input * @return mixed[] */ - public function generate($e): array + public function generate(mixed $input): array { return [ - 'error' => $e->getMessage(), + 'error' => $input->getMessage(), ]; } diff --git a/src/Generator/Impl/IGenerator.php b/src/Generator/Impl/IGenerator.php index 7c890dc..be07189 100644 --- a/src/Generator/Impl/IGenerator.php +++ b/src/Generator/Impl/IGenerator.php @@ -9,9 +9,8 @@ interface IGenerator public const EXCEPTION = '!'; /** - * @param mixed $input * @return mixed[] */ - public function generate($input): array; + public function generate(mixed $input): array; } diff --git a/src/Grub.php b/src/Grub.php index ed74dad..a8a9ada 100644 --- a/src/Grub.php +++ b/src/Grub.php @@ -20,6 +20,9 @@ public static function boot(): void $dag->serve(); } + /** + * @return mixed[] + */ protected function getParameters(): array { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); @@ -37,4 +40,5 @@ protected function getParameters(): array 'debug' => getenv('DEBUG') === '1', ]; } + } diff --git a/src/Handler/Executor.php b/src/Handler/Executor.php index b2ba772..121ee8c 100644 --- a/src/Handler/Executor.php +++ b/src/Handler/Executor.php @@ -12,11 +12,9 @@ class Executor { - /** @var Router */ - private $router; + private Router $router; - /** @var Generator */ - private $generator; + private Generator $generator; public function __construct(Router $router, Generator $generator) { diff --git a/src/Http/DagResponse.php b/src/Http/DagResponse.php index 685f60e..0ce6a05 100644 --- a/src/Http/DagResponse.php +++ b/src/Http/DagResponse.php @@ -9,19 +9,16 @@ final class DagResponse { - /** @var mixed */ - private $payload; + private mixed $payload; /** @var mixed[] */ - private $headers = []; + private array $headers = []; - /** @var int */ - private $statusCode = 200; + private int $statusCode = 200; - /** @var string */ - private $contentType = 'application/json'; + private string $contentType = 'application/json'; - public function __construct($payload) + public function __construct(mixed $payload) { $this->payload = $payload; } diff --git a/src/Router/Router.php b/src/Router/Router.php index 19242a2..6d9a94f 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -9,27 +9,34 @@ class Router { /** @var mixed[] */ - private $endpoints = []; + private array $endpoints = []; - /** @var RouteList */ - private $router; + private RouteList $router; public function __construct() { $this->router = new RouteList(); } + /** + * @param mixed[] $endpoint + */ public function add(string $path, array $endpoint): void { $this->endpoints[$path] = $endpoint; $this->router->addRoute($path, ['path' => $path]); } + /** + * @return mixed[]|null + */ public function match(Request $request): ?array { $endpoint = $this->router->match($request); - if ($endpoint === null) return null; + if ($endpoint === null) { + return null; + } return $this->endpoints[$endpoint['path']]; } diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 378fbf0..00d2119 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -7,7 +7,10 @@ class Arrays extends CArrays { - public static function toArray($data): array + /** + * @return mixed[] + */ + public static function toArray(mixed $data): array { return json_decode(json_encode($data), true); } diff --git a/src/Utils/Zeit.php b/src/Utils/Zeit.php index a21c2df..f66dd4e 100644 --- a/src/Utils/Zeit.php +++ b/src/Utils/Zeit.php @@ -7,7 +7,7 @@ class Zeit public static function now(): bool { - return !empty($_ENV['NOW_REGION'] ?? false); + return (bool) getenv('NOW_REGION'); } } diff --git a/tests/Cases/GeneratorTest.phpt b/tests/Cases/GeneratorTest.phpt new file mode 100644 index 0000000..1f3a813 --- /dev/null +++ b/tests/Cases/GeneratorTest.phpt @@ -0,0 +1,34 @@ +generate(null); + Assert::same(['error' => 'Endpoint not found'], $result); +}); + +Toolkit::test(function (): void { + $generator = new ExceptionGenerator(); + $result = $generator->generate(new RuntimeException('Test error')); + Assert::same(['error' => 'Test error'], $result); +}); + +Toolkit::test(function (): void { + $generator = new Generator(); + $fallback = new FallbackGenerator(); + $generator->add(IGenerator::FALLBACK, $fallback); + + $result = $generator->generate(IGenerator::FALLBACK, null); + Assert::same(['error' => 'Endpoint not found'], $result); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..86c92a0 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,10 @@ +