diff --git a/.gitattributes b/.gitattributes index ddd8e04..5cd84a1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,14 @@ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html -# Ignore all test and documentation with "export-ignore". -/.gitattributes export-ignore -/.gitignore export-ignore -/.travis.yml export-ignore -/phpunit.xml.dist export-ignore -/.scrutinizer.yml export-ignore -/tests export-ignore -/docs export-ignore -/.github export-ignore -/.editorconfig export-ignore +# Keep development-only files out of the distributed package. +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.phpunit-watcher.yml export-ignore +/CHANGELOG.md export-ignore +/docs.md export-ignore +/phpstan.neon export-ignore +/phpunit.xml export-ignore +/tests export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..332e83f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 + +updates: + - package-ecosystem: composer + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 5 + labels: + - dependencies + # The library itself only requires php-parser. Everything else here is test + # tooling, and the CI matrix already pins the Laravel versions it exercises. + ignore: + - dependency-name: "orchestra/testbench" + update-types: ["version-update:semver-major"] + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + labels: + - dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3d7758..9cbdb02 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,105 +1,95 @@ name: tests -on: [push, pull_request] - -env: - COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --no-suggest --prefer-dist -o -n" +on: + push: + branches: [master] + pull_request: + workflow_dispatch: jobs: - laravel-tests: + tests: strategy: fail-fast: false matrix: - php-version: [7.4, 8.0] - laravel-version: [6, 7, 8] - os: [ubuntu-latest] - experimental: [false] - dependencies: [locked] include: - - php-version: 7.4 - laravel-version: 8 - dependencies: highest - os: ubuntu-latest - experimental: false - - # - php-version: 7.4 - # laravel-version: 8 - # os: windows-latest - # dependencies: locked - # experimental: false - - - php-version: 7.4 - laravel-version: 8 - os: macos-latest - dependencies: locked - experimental: false - - name: PHP ${{ matrix.php-version }} - L${{ matrix.laravel-version }} - ${{ matrix.os }} - ${{ matrix.dependencies }} + # Laravel 10 (testbench 8) + - { php: '8.1', laravel: '10.*', testbench: '8.*', os: ubuntu-latest } + - { php: '8.2', laravel: '10.*', testbench: '8.*', os: ubuntu-latest } + - { php: '8.3', laravel: '10.*', testbench: '8.*', os: ubuntu-latest } + # Laravel 11 (testbench 9) + - { php: '8.2', laravel: '11.*', testbench: '9.*', os: ubuntu-latest } + - { php: '8.3', laravel: '11.*', testbench: '9.*', os: ubuntu-latest } + - { php: '8.4', laravel: '11.*', testbench: '9.*', os: ubuntu-latest } + # Laravel 12 (testbench 10) + - { php: '8.2', laravel: '12.*', testbench: '10.*', os: ubuntu-latest } + - { php: '8.3', laravel: '12.*', testbench: '10.*', os: ubuntu-latest } + - { php: '8.4', laravel: '12.*', testbench: '10.*', os: ubuntu-latest } + # Laravel 13 (testbench 11, PHP 8.3+). statamic/cms 6 allows ^13.0, + # so this is a version real downstream users are already on. + - { php: '8.3', laravel: '13.*', testbench: '11.*', os: ubuntu-latest } + - { php: '8.4', laravel: '13.*', testbench: '11.*', os: ubuntu-latest } + # Keep an eye on the other host we claim to work on + - { php: '8.4', laravel: '12.*', testbench: '10.*', os: macos-latest } + + name: PHP ${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-version }} + php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd coverage: none - ini-values: "memory_limit=-1" - tools: composer + ini-values: memory_limit=-1, error_reporting=E_ALL + tools: composer:v2 - name: Determine composer cache directory - id: determine-composer-cache-directory - run: 'echo "::set-output name=directory::$(composer config cache-dir)"' + id: composer-cache + run: echo "directory=$(composer config cache-dir)" >> "$GITHUB_OUTPUT" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: ${{ steps.determine-composer-cache-directory.outputs.directory }} - key: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer- - - - name: Setup problem matchers for PHP - run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + path: ${{ steps.composer-cache.outputs.directory }} + key: composer-${{ matrix.os }}-php${{ matrix.php }}-laravel${{ matrix.laravel }}-${{ hashFiles('composer.json') }} + restore-keys: composer-${{ matrix.os }}-php${{ matrix.php }}-laravel${{ matrix.laravel }}- + + - name: Allow installing EOL Laravel branches + # Laravel 10 and 11 are past security support, so every release in those + # branches carries an open advisory and Composer 2.9+ declines to install + # them at all. This is a compatibility matrix rather than a deployment, + # and plenty of applications are still on those versions, so we want to + # know whether Archetype works there. Laravel 12 installs unmodified. + if: matrix.laravel != '12.*' + run: composer config policy.advisories.block false + + - name: Install dependencies + run: | + composer require --dev --no-update --no-interaction \ + "laravel/framework:${{ matrix.laravel }}" \ + "orchestra/testbench:${{ matrix.testbench }}" + composer update --prefer-dist --no-interaction --no-progress - - name: Install highest dependencies from composer.json - if: matrix.dependencies == 'highest' - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer config platform --unset && composer update ${{ env.COMPOSER_FLAGS }} + - name: Run tests + run: vendor/bin/pest - - name: Install lowest dependencies from composer.json - if: matrix.dependencies == 'lowest' - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer install ${{ env.COMPOSER_FLAGS }} --prefer-lowest + static-analysis: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - - name: Install dependencies from composer.lock - if: matrix.dependencies == 'locked' - uses: nick-invision/retry@v1 + - uses: shivammathur/setup-php@v2 with: - timeout_minutes: 5 - max_attempts: 5 - command: composer install ${{ env.COMPOSER_FLAGS }} + php-version: '8.3' + coverage: none + tools: composer:v2 - - name: Select Laravel version - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer require "laravel/framework:${{ matrix.laravel-version }}.*" --no-update + - run: composer update --prefer-dist --no-interaction --no-progress - - name: Run tests - run: vendor/bin/pest --verbose + - run: vendor/bin/phpstan analyse --no-progress diff --git a/.gitignore b/.gitignore index 310c8bc..cf7d6e5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ sketches/ .vscode/ vendor/ .phpunit.result.cache +.phpunit.cache/ logs composer.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2d5fa21 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +All notable changes to `ajthinking/archetype` are documented here. + +This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [2.0.1] - 2026-08-25 + +Maintenance only. No API changes, and nothing here can break existing usage. + +### Added + +- Support for PHP 8.1 through 8.4, and for Laravel 10, 11, 12 and 13 — now + verified in CI across twelve combinations rather than assumed. Laravel 13 is + included because `statamic/cms` 6 already allows it. +- A `php` requirement (`^8.1`) in `composer.json`. The package never declared + one, so Composer could not warn anybody. 8.1 is the floor the code actually + needs and the whole range CI exercises; the upper bound is deliberate, because + PHP 9 removes dynamic properties and the AST node-identity mechanism still + relies on them. +- `LICENSE.md`. The package has always been MIT; the file was missing. +- This changelog. +- `.github/dependabot.yml`, so dependency updates are configured rather than + running on defaults. + +### Changed + +- The test suite now runs against a pinned application skeleton in + `tests/fixtures/laravel` instead of copying one out of `vendor/laravel/laravel` + at runtime. Laravel 11 removed most of the files the suite relied on + (`app/Console/Kernel.php`, `app/Exceptions/Handler.php`, `RouteServiceProvider` + and the middleware), which made the old approach untestable on current Laravel. + `laravel/laravel` is no longer a dev dependency. +- CI runs on `actions/checkout@v4` and `actions/cache@v4`, and no longer uses the + `::set-output` command that GitHub has disabled. +- `phpunit.xml` migrated to the PHPUnit 10+ schema. +- `Archetype\Tests\` moved out of the production autoloader into `autoload-dev`. +- `minimum-stability` is now `stable`. + +### Fixed + +- Seven implicit-nullable parameters that raised deprecation notices on PHP 8.4 + (`PHPFile::namespace()`, `Namespace_::namespace()`, `FileInput::load()`, + `InputInterface::load()`, `EndpointProvider::__construct()`, + `PHPParserPropertyMap::propertyMap()` and `ASTQueryBuilder::traverseIntoClass()`). +- `getReflection()` now catches `Throwable` rather than `Exception`. A file whose + parent class or trait cannot be autoloaded raises an `Error`, which previously + escaped and killed the entire query instead of skipping that one file. This + affects `LaravelFile::models()`, `controllers()` and `serviceProviders()`. +- `tests/Pest.php` no longer defines a global `context()` helper. Laravel 11 ships + its own `context()`, and the collision brought the whole suite down before a + single test could run. + +## [2.0.0] - 2024-05-11 + +### Changed + +- **Breaking:** upgraded to `nikic/php-parser` ^5.0. Thanks to @jasonvarga (#85). + +## [1.1.5] - 2022-08-24 + +Last release of the 1.x line, which requires `nikic/php-parser` ^4.11. + +1.x cannot be installed alongside anything that needs php-parser 5 — including +Pest 3 and newer. If Composer refuses to resolve `ajthinking/archetype`, upgrade +to 2.x. + +[Unreleased]: https://github.com/ajthinking/archetype/compare/v2.0.1...HEAD +[2.0.1]: https://github.com/ajthinking/archetype/compare/v2.0.0...v2.0.1 +[2.0.0]: https://github.com/ajthinking/archetype/compare/v1.1.5...v2.0.0 +[1.1.5]: https://github.com/ajthinking/archetype/releases/tag/v1.1.5 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2325319 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) Anders Jürisoo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json index 9cf6609..3795160 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,13 @@ } ], "require": { + "php": "^8.1", "nikic/php-parser": "^5.0" }, "require-dev": { - "laravel/laravel": "^6.0 || ^7.0 || ^8.0 || ^9.0", - "orchestra/testbench": "^4.0 || ^5.0 || ^6.0", - "pestphp/pest": "^1.21", - "phpstan/phpstan": "^1.6", - "phpunit/phpunit": "^8.0 || ^9.5" + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "pestphp/pest": "^2.0 || ^3.0", + "phpstan/phpstan": "^1.11 || ^2.0" }, "config": { "optimize-autoloader": true, @@ -44,15 +43,30 @@ }, "autoload": { "psr-4": { - "Archetype\\": "src/", - "Archetype\\Tests\\": "tests/" - } + "Archetype\\": "src/" + }, + "exclude-from-classmap": [ + "src/snippets/" + ] }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" - } + "Archetype\\Tests\\": "tests/", + "App\\": "tests/fixtures/laravel/app/" + }, + "files": [ + "tests/stubs/laravel-sanctum.php" + ], + "exclude-from-classmap": [ + "tests/fixtures/laravel/database/", + "tests/fixtures/laravel/public/", + "tests/stubs/" + ] + }, + "scripts": { + "test": "pest", + "analyse": "phpstan analyse" }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true } diff --git a/phpstan.neon b/phpstan.neon index f3cb1ef..3c5177a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,5 +3,9 @@ parameters: paths: - src - tests + excludePaths: + # A pinned copy of a Laravel application skeleton, used as test input. + # It is data for the parser, not code we maintain. + - tests/fixtures/* ignoreErrors: - - '#Dummy not found#' \ No newline at end of file + - '#Dummy not found#' diff --git a/phpunit.xml b/phpunit.xml index 7f1309d..c1aa23c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,12 @@ + stopOnFailure="false" + cacheDirectory=".phpunit.cache"> ./tests/Unit @@ -19,9 +16,9 @@ ./tests/Feature - - + + ./src - - + + diff --git a/readme.md b/readme.md index 9d41311..e900a38 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ ### Enabling Rapid-Application-Development-tools, PR-bots, code analyzers and other things -![tests](https://github.com/ajthinking/archetype/workflows/tests/badge.svg) +[![tests](https://github.com/ajthinking/archetype/actions/workflows/tests.yml/badge.svg)](https://github.com/ajthinking/archetype/actions/workflows/tests.yml) ![version](https://img.shields.io/packagist/v/ajthinking/archetype?color=blue) [![Total Downloads](https://img.shields.io/packagist/dt/ajthinking/archetype.svg)](https://packagist.org/packages/ajthinking/archetype) @@ -16,6 +16,18 @@ composer require ajthinking/archetype > That's it! Check out introduction of concepts below or review the [API examples](docs.md) + +## Supported versions + +| Archetype | PHP | Laravel | php-parser | +| --------- | ------- | -------- | ---------- | +| 2.x | 8.1–8.4 | 10, 11, 12, 13 | ^5.0 | +| 1.x | 7.4–8.1 | 6–9 | ^4.11 | + +> **On 1.x?** Upgrade to 2.x. Version 1 pins `nikic/php-parser` to `^4.11`, which +> cannot be installed alongside anything that needs php-parser 5 — Pest 3 and +> newer included. If Composer refuses to resolve `ajthinking/archetype`, that is +> why. The 1.x → 2.x upgrade requires no changes to your own code. ## `PHPFile` read/write API @@ -194,10 +206,10 @@ To see *all* offending files run `php artisan archetype:errors`. To ignore files php artisan vendor:publish --provider="Archetype\ServiceProvider" ``` -## Requirmenst +## Requirements * UNIX filesystem -* PHP >= 7.4 -* Laravel >= 7 +* PHP >= 8.1 +* Laravel >= 10 ## Contributing PRs and issues are welcome :pray: Feel free to take a stab at an [incomplete test](https://github.com/ajthinking/archetype/search?q=%24this-%3EmarkTestIncomplete). @@ -221,7 +233,3 @@ MIT ## Like this package? Star it :star: - -[Say hi: @ajthinking :gem:](https://twitter.com/ajthinking) - -[Github Sponsors :octocat::heart:](https://github.com/sponsors/ajthinking) diff --git a/src/Drivers/FileInput.php b/src/Drivers/FileInput.php index dabf4fb..f0e470e 100644 --- a/src/Drivers/FileInput.php +++ b/src/Drivers/FileInput.php @@ -30,7 +30,7 @@ public function readPath($path = null): self return $this; } - public function load(string $location = null) + public function load(?string $location = null) { $this->extractPathProperties($location); diff --git a/src/Drivers/InputInterface.php b/src/Drivers/InputInterface.php index 9769209..93a3f0d 100644 --- a/src/Drivers/InputInterface.php +++ b/src/Drivers/InputInterface.php @@ -4,5 +4,5 @@ interface InputInterface { - public function load(string $path = null); + public function load(?string $path = null); } diff --git a/src/Endpoints/EndpointProvider.php b/src/Endpoints/EndpointProvider.php index c83a793..113842e 100644 --- a/src/Endpoints/EndpointProvider.php +++ b/src/Endpoints/EndpointProvider.php @@ -13,7 +13,7 @@ abstract class EndpointProvider protected $directives; - public function __construct(PHPFile $file = null) + public function __construct(?PHPFile $file = null) { $this->file = $file; diff --git a/src/Endpoints/PHP/Namespace_.php b/src/Endpoints/PHP/Namespace_.php index bbf36d5..9eb93b4 100644 --- a/src/Endpoints/PHP/Namespace_.php +++ b/src/Endpoints/PHP/Namespace_.php @@ -20,7 +20,7 @@ class Namespace_ extends EndpointProvider * @param string $value * @return mixed */ - public function namespace(string $value = null) + public function namespace(?string $value = null) { if ($this->file->directive('remove')) { return $this->remove(); diff --git a/src/Endpoints/PHP/ReflectionProxy.php b/src/Endpoints/PHP/ReflectionProxy.php index d40041e..c9e9f8e 100644 --- a/src/Endpoints/PHP/ReflectionProxy.php +++ b/src/Endpoints/PHP/ReflectionProxy.php @@ -4,7 +4,7 @@ use Archetype\Endpoints\EndpointProvider; use ReflectionClass; -use Exception; +use Throwable; class ReflectionProxy extends EndpointProvider { @@ -20,7 +20,11 @@ public function getReflection() try { return $class ? new ReflectionClass($class) : null; - } catch (Exception $e) { + } catch (Throwable $e) { + // Autoloading the class can fail outright rather than just miss — + // a missing parent class or trait raises an Error, not an Exception. + // A file we cannot reflect on is one we skip, never one that kills + // the whole query. return null; } } diff --git a/src/PHPFile.php b/src/PHPFile.php index ff23c76..722a05e 100644 --- a/src/PHPFile.php +++ b/src/PHPFile.php @@ -123,7 +123,7 @@ public function useTrait($value = null) return $handler->useTrait($value); } - public function namespace(string $value = null) + public function namespace(?string $value = null) { $handler = new Namespace_($this); return $handler->namespace($value); diff --git a/src/Support/AST/ASTQueryBuilder.php b/src/Support/AST/ASTQueryBuilder.php index 1195e62..5efd862 100644 --- a/src/Support/AST/ASTQueryBuilder.php +++ b/src/Support/AST/ASTQueryBuilder.php @@ -72,7 +72,7 @@ public function __get(string $name) throw new Exception("Could not find a property $property in the ASTQueryBuilder!"); } - public function traverseIntoClass($expectedClass, string $path = null): self + public function traverseIntoClass($expectedClass, ?string $path = null): self { $steps = $path ? collect(explode('->', $path)) : collect(); diff --git a/src/Traits/PHPParserClassMap.php b/src/Traits/PHPParserClassMap.php index 0711c91..12a5740 100644 --- a/src/Traits/PHPParserClassMap.php +++ b/src/Traits/PHPParserClassMap.php @@ -90,7 +90,8 @@ trait PHPParserClassMap 'string' => \PhpParser\Node\Scalar\String_::class, 'switch' => \PhpParser\Node\Stmt\Switch_::class, 'ternary' => \PhpParser\Node\Expr\Ternary::class, - 'throw' => \PhpParser\Node\Stmt\Throw_::class, + // php-parser 5 dropped Stmt\Throw_; throw is only ever an expression now. + 'throw' => \PhpParser\Node\Expr\Throw_::class, 'trait' => \PhpParser\Node\Stmt\Trait_::class, 'traitUse' => \PhpParser\Node\Stmt\TraitUse::class, 'traitUseAdaptation' => \PhpParser\Node\Stmt\TraitUseAdaptation::class, diff --git a/src/Traits/PHPParserPropertyMap.php b/src/Traits/PHPParserPropertyMap.php index 530718f..34befe4 100644 --- a/src/Traits/PHPParserPropertyMap.php +++ b/src/Traits/PHPParserPropertyMap.php @@ -4,7 +4,7 @@ trait PHPParserPropertyMap { - public function propertyMap(string $property = null) + public function propertyMap(?string $property = null) { $map = [ 'expr', diff --git a/src/config/archetype.php b/src/config/archetype.php index 3e97415..28a9992 100644 --- a/src/config/archetype.php +++ b/src/config/archetype.php @@ -59,8 +59,8 @@ 'class_root' => '', // Laravel - 'commands_root' => 'app/HTTP/Controllers', - 'controllers_root' => 'app/HTTP/Controllers', + 'commands_root' => 'app/Console/Commands', + 'controllers_root' => 'app/Http/Controllers', 'factories_root' => 'database/factories', 'migrations_root' => 'database/migrations', 'models_root' => 'app/Models', diff --git a/tests/Pest.php b/tests/Pest.php index fe9c17a..85c43af 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -4,18 +4,4 @@ use Archetype\Tests\TestCase; -if (!function_exists('context')) { - function context(string $description, Closure $closure) - { - $closure(); - } -} - -if (!function_exists('describe')) { - function describe(string $description, Closure $closure) - { - $closure(); - } -} - -uses(TestCase::class)->in(__DIR__); \ No newline at end of file +uses(TestCase::class)->in(__DIR__); diff --git a/tests/Support/TestablePHPFile.php b/tests/Support/TestablePHPFile.php index 66e5f58..1d39c2b 100644 --- a/tests/Support/TestablePHPFile.php +++ b/tests/Support/TestablePHPFile.php @@ -32,7 +32,7 @@ public function assertDirectives($expectedMap) return $this; } - public function assertExtends(string $expected = null) + public function assertExtends(?string $expected = null) { assertEquals($expected, $this->extends()); @@ -58,21 +58,21 @@ public function assertClassConstant(string $name, $value) return $this; } - public function assertImplements(array $expected = null) + public function assertImplements(?array $expected = null) { assertEquals($expected, $this->implements()); return $this; } - public function assertClassName(string $name = null) + public function assertClassName(?string $name = null) { assertEquals($name, $this->className()); return $this; } - public function assertMethodNames(array $expected = null) + public function assertMethodNames(?array $expected = null) { assertEquals($expected, $this->methodNames()); @@ -137,14 +137,14 @@ public function assertContains(string $string) return $this; } - public function assertUse(array $expected = null) + public function assertUse(?array $expected = null) { assertEquals($expected, $this->use()); return $this; } - public function assertUseTrait(array $expected = null) + public function assertUseTrait(?array $expected = null) { assertEquals($expected, $this->useTrait()); @@ -183,7 +183,7 @@ public function assertMultilineArray($name) return $this; } - public function assertNamespace(string $expected = null) + public function assertNamespace(?string $expected = null) { assertEquals($expected, $this->namespace()); diff --git a/tests/TestCase.php b/tests/TestCase.php index 7af7863..b4c3780 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -43,11 +43,20 @@ protected function registerTestFacades() protected function setupLaravelDirectories() { + $package = dirname(__DIR__); + $fixtures = __DIR__.'/fixtures/laravel'; + File::ensureDirectoryExists(base_path('vendor/ajthinking/archetype/src/snippets')); - File::copyDirectory(base_path('./../../../../src/snippets/'), base_path('vendor/ajthinking/archetype/src/snippets')); + File::copyDirectory($package.'/src/snippets', base_path('vendor/ajthinking/archetype/src/snippets')); + + // The suite asserts on exact file counts and on the contents of the + // application skeleton. Testbench ships its own skeleton, and Laravel + // reshapes it every major version, so replace it wholesale with the + // pinned fixture app to keep those assertions meaningful. + File::deleteDirectory(base_path('app')); - collect(['app', 'database/migrations','public'])->each(function ($path) { - File::copyDirectory(base_path('./../../../../vendor/laravel/laravel/' . $path), base_path($path)); + collect(['app', 'database/migrations', 'public'])->each(function ($path) use ($fixtures) { + File::copyDirectory($fixtures.'/'.$path, base_path($path)); }); } diff --git a/tests/Unit/Support/AST/ASTQueryBuilderTest.php b/tests/Unit/Support/AST/ASTQueryBuilderTest.php index 5687269..3a65bd5 100644 --- a/tests/Unit/Support/AST/ASTQueryBuilderTest.php +++ b/tests/Unit/Support/AST/ASTQueryBuilderTest.php @@ -59,6 +59,15 @@ ->assertMatchCount(1); }); +it('can query throw expressions', function() { + // php-parser 5 removed Stmt\Throw_ in favour of Expr\Throw_, so the short + // name has to resolve to the expression node. + PHPFile::fromString('throw new Exception("nope");') + ->astQuery() + ->throw() + ->assertMatchCount(1); +}); + it('can traverse into property when result is an array', function() { PHPFile::fromString('1;2;')->astQuery() // Two(!) Expression:s ->expr @@ -73,7 +82,7 @@ ->assertMatchCount(1); }); -context('when searching method chains', function() { +describe('when searching method chains', function() { it('will match all methodCalls by default', function() { PHPFile::fromString('$lets->go()->go()->go()') ->astQuery() diff --git a/tests/fixtures/laravel/app/Console/Kernel.php b/tests/fixtures/laravel/app/Console/Kernel.php new file mode 100644 index 0000000..d8bc1d2 --- /dev/null +++ b/tests/fixtures/laravel/app/Console/Kernel.php @@ -0,0 +1,32 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/tests/fixtures/laravel/app/Exceptions/Handler.php b/tests/fixtures/laravel/app/Exceptions/Handler.php new file mode 100644 index 0000000..8e7fbd1 --- /dev/null +++ b/tests/fixtures/laravel/app/Exceptions/Handler.php @@ -0,0 +1,41 @@ +> + */ + protected $dontReport = [ + // + ]; + + /** + * A list of the inputs that are never flashed for validation exceptions. + * + * @var array + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + * + * @return void + */ + public function register() + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/tests/fixtures/laravel/app/Http/Controllers/Controller.php b/tests/fixtures/laravel/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..a0a2a8a --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Fruitcake\Cors\HandleCors::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array> + */ + protected $middlewareGroups = [ + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/Authenticate.php b/tests/fixtures/laravel/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..704089a --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/Authenticate.php @@ -0,0 +1,21 @@ +expectsJson()) { + return route('login'); + } + } +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/EncryptCookies.php b/tests/fixtures/laravel/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..867695b --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/tests/fixtures/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php new file mode 100644 index 0000000..74cbd9a --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/RedirectIfAuthenticated.php b/tests/fixtures/laravel/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..a2813a0 --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,32 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/TrimStrings.php b/tests/fixtures/laravel/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..88cadca --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,19 @@ + + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/TrustHosts.php b/tests/fixtures/laravel/app/Http/Middleware/TrustHosts.php new file mode 100644 index 0000000..7186414 --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/TrustHosts.php @@ -0,0 +1,20 @@ + + */ + public function hosts() + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/TrustProxies.php b/tests/fixtures/laravel/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..3391630 --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,28 @@ +|string|null + */ + protected $proxies; + + /** + * The headers that should be used to detect proxies. + * + * @var int + */ + protected $headers = + Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; +} diff --git a/tests/fixtures/laravel/app/Http/Middleware/VerifyCsrfToken.php b/tests/fixtures/laravel/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..9e86521 --- /dev/null +++ b/tests/fixtures/laravel/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/tests/fixtures/laravel/app/Models/User.php b/tests/fixtures/laravel/app/Models/User.php new file mode 100644 index 0000000..8996368 --- /dev/null +++ b/tests/fixtures/laravel/app/Models/User.php @@ -0,0 +1,44 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + ]; +} diff --git a/tests/fixtures/laravel/app/Providers/AppServiceProvider.php b/tests/fixtures/laravel/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..ee8ca5b --- /dev/null +++ b/tests/fixtures/laravel/app/Providers/AppServiceProvider.php @@ -0,0 +1,28 @@ + + */ + protected $policies = [ + // 'App\Models\Model' => 'App\Policies\ModelPolicy', + ]; + + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + $this->registerPolicies(); + + // + } +} diff --git a/tests/fixtures/laravel/app/Providers/BroadcastServiceProvider.php b/tests/fixtures/laravel/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..395c518 --- /dev/null +++ b/tests/fixtures/laravel/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,21 @@ +> + */ + protected $listen = [ + Registered::class => [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/tests/fixtures/laravel/app/Providers/RouteServiceProvider.php b/tests/fixtures/laravel/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..3bd3c81 --- /dev/null +++ b/tests/fixtures/laravel/app/Providers/RouteServiceProvider.php @@ -0,0 +1,63 @@ +configureRateLimiting(); + + $this->routes(function () { + Route::prefix('api') + ->middleware('api') + ->namespace($this->namespace) + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web.php')); + }); + } + + /** + * Configure the rate limiters for the application. + * + * @return void + */ + protected function configureRateLimiting() + { + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); + }); + } +} diff --git a/tests/fixtures/laravel/database/migrations/2014_10_12_000000_create_users_table.php b/tests/fixtures/laravel/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..621a24e --- /dev/null +++ b/tests/fixtures/laravel/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/tests/fixtures/laravel/database/migrations/2014_10_12_100000_create_password_resets_table.php b/tests/fixtures/laravel/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 0000000..0ee0a36 --- /dev/null +++ b/tests/fixtures/laravel/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,32 @@ +string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('password_resets'); + } +} diff --git a/tests/fixtures/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/tests/fixtures/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..6aa6d74 --- /dev/null +++ b/tests/fixtures/laravel/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/tests/fixtures/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/tests/fixtures/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..4315e16 --- /dev/null +++ b/tests/fixtures/laravel/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,36 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('personal_access_tokens'); + } +} diff --git a/tests/fixtures/laravel/public/index.php b/tests/fixtures/laravel/public/index.php new file mode 100644 index 0000000..1d69f3a --- /dev/null +++ b/tests/fixtures/laravel/public/index.php @@ -0,0 +1,55 @@ +make(Kernel::class); + +$response = $kernel->handle( + $request = Request::capture() +)->send(); + +$kernel->terminate($request, $response); diff --git a/tests/stubs/laravel-sanctum.php b/tests/stubs/laravel-sanctum.php new file mode 100644 index 0000000..f6860cd --- /dev/null +++ b/tests/stubs/laravel-sanctum.php @@ -0,0 +1,16 @@ +