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
-
+[](https://github.com/ajthinking/archetype/actions/workflows/tests.yml)

[](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 @@
+