diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 00000000..9634a0ed --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,12 @@ +name: issues + +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + help-wanted: + uses: laravel/.github/.github/workflows/issues.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fdc6d71..912753c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,25 +2,28 @@ name: tests on: push: + branches: + - master + - '*.x' pull_request: schedule: - cron: '0 0 * * *' jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: true matrix: - php: ['8.0', '8.1'] + php: ['8.0', 8.1, 8.2] stability: [prefer-lowest, prefer-stable] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/composer.json b/composer.json index cd436282..f327bb33 100644 --- a/composer.json +++ b/composer.json @@ -16,31 +16,31 @@ ], "require": { "php": "^8.0.2", - "illuminate/auth": "^9.21", - "illuminate/broadcasting": "^9.21", - "illuminate/bus": "^9.21", - "illuminate/cache": "^9.21", - "illuminate/collections": "^9.21", - "illuminate/config": "^9.21", - "illuminate/console": "^9.21", - "illuminate/container": "^9.21", - "illuminate/contracts": "^9.21", - "illuminate/database": "^9.21", - "illuminate/encryption": "^9.21", - "illuminate/events": "^9.21", - "illuminate/filesystem": "^9.21", - "illuminate/hashing": "^9.21", - "illuminate/http": "^9.21", - "illuminate/macroable": "^9.21", - "illuminate/pagination": "^9.21", - "illuminate/pipeline": "^9.21", - "illuminate/queue": "^9.21", - "illuminate/support": "^9.21", - "illuminate/testing": "^9.21", - "illuminate/translation": "^9.21", - "illuminate/validation": "^9.21", - "illuminate/view": "^9.21", - "illuminate/log": "^9.21", + "illuminate/auth": "^9.36.3", + "illuminate/broadcasting": "^9.36.3", + "illuminate/bus": "^9.36.3", + "illuminate/cache": "^9.36.3", + "illuminate/collections": "^9.36.3", + "illuminate/config": "^9.36.3", + "illuminate/console": "^9.36.3", + "illuminate/container": "^9.36.3", + "illuminate/contracts": "^9.36.3", + "illuminate/database": "^9.36.3", + "illuminate/encryption": "^9.36.3", + "illuminate/events": "^9.36.3", + "illuminate/filesystem": "^9.36.3", + "illuminate/hashing": "^9.36.3", + "illuminate/http": "^9.36.3", + "illuminate/macroable": "^9.36.3", + "illuminate/pagination": "^9.36.3", + "illuminate/pipeline": "^9.36.3", + "illuminate/queue": "^9.36.3", + "illuminate/support": "^9.36.3", + "illuminate/testing": "^9.36.3", + "illuminate/translation": "^9.36.3", + "illuminate/validation": "^9.36.3", + "illuminate/view": "^9.36.3", + "illuminate/log": "^9.36.3", "dragonmantank/cron-expression": "^3.1", "nikic/fast-route": "^1.3", "symfony/console": "^6.0", diff --git a/src/Application.php b/src/Application.php index 77fda165..31cfcec6 100644 --- a/src/Application.php +++ b/src/Application.php @@ -111,6 +111,13 @@ class Application extends Container */ public $router; + /** + * The array of terminating callbacks. + * + * @var callable[] + */ + protected $terminatingCallbacks = []; + /** * Create a new Lumen application instance. * @@ -162,7 +169,7 @@ public function bootstrapRouter() */ public function version() { - return 'Lumen (9.1.1) (Laravel Components ^9.21)'; + return 'Lumen (9.1.6) (Laravel Components ^9.36.3)'; } /** @@ -200,6 +207,26 @@ public function environment() return $env; } + /** + * Determine if the application is in the local environment. + * + * @return bool + */ + public function isLocal() + { + return $this->environment() === 'local'; + } + + /** + * Determine if the application is in the production environment. + * + * @return bool + */ + public function isProduction() + { + return $this->environment() === 'production'; + } + /** * Determine if the given service provider is loaded. * @@ -906,7 +933,7 @@ public function runningInConsole() */ public function runningUnitTests() { - return $this->environment() == 'testing'; + return $this->environment() === 'testing'; } /** @@ -989,6 +1016,16 @@ public function getLocale() return $this['config']->get('app.locale'); } + /** + * Get the current application fallback locale. + * + * @return string + */ + public function getFallbackLocale() + { + return $this['config']->get('app.fallback_locale'); + } + /** * Set the current application locale. * @@ -1012,6 +1049,35 @@ public function isLocale($locale) return $this->getLocale() == $locale; } + /** + * Register a terminating callback with the application. + * + * @param callable|string $callback + * @return $this + */ + public function terminating($callback) + { + $this->terminatingCallbacks[] = $callback; + + return $this; + } + + /** + * Terminate the application. + * + * @return void + */ + public function terminate() + { + $index = 0; + + while ($index < count($this->terminatingCallbacks)) { + $this->call($this->terminatingCallbacks[$index]); + + $index++; + } + } + /** * Register the core container aliases. * diff --git a/src/Concerns/RoutesRequests.php b/src/Concerns/RoutesRequests.php index cbb3daaf..fdac8696 100644 --- a/src/Concerns/RoutesRequests.php +++ b/src/Concerns/RoutesRequests.php @@ -120,6 +120,8 @@ public function run($request = null) if (count($this->middleware) > 0) { $this->callTerminableMiddleware($response); } + + $this->app->terminate(); } /** diff --git a/src/Console/Kernel.php b/src/Console/Kernel.php index dd1a12e7..ee8d697b 100644 --- a/src/Console/Kernel.php +++ b/src/Console/Kernel.php @@ -113,14 +113,18 @@ public function handle($input, $output = null) try { $this->app->boot(); - return $this->getArtisan()->run($input, $output); + $status = $this->getArtisan()->run($input, $output); } catch (Throwable $e) { $this->reportException($e); $this->renderException($output, $e); - return 1; + $status = 1; } + + $this->terminate($input, $status); + + return $status; } /** @@ -142,7 +146,7 @@ public function bootstrap() */ public function terminate($input, $status) { - // + $this->app->terminate(); } /** diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index 91c82c52..5d74b589 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -111,7 +111,7 @@ public function render($request, Throwable $e) } elseif ($e instanceof ModelNotFoundException) { $e = new NotFoundHttpException($e->getMessage(), $e); } elseif ($e instanceof AuthorizationException) { - $e = new HttpException(403, $e->getMessage()); + $e = new HttpException($e->status() ?? 403, $e->getMessage()); } elseif ($e instanceof ValidationException && $e->getResponse()) { return $e->getResponse(); } diff --git a/src/Routing/ProvidesConvenienceMethods.php b/src/Routing/ProvidesConvenienceMethods.php index 1e246001..cded24f3 100644 --- a/src/Routing/ProvidesConvenienceMethods.php +++ b/src/Routing/ProvidesConvenienceMethods.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; @@ -63,27 +64,41 @@ public function validate(Request $request, array $rules, array $messages = [], a { $validator = $this->getValidationFactory()->make($request->all(), $rules, $messages, $customAttributes); - try { - $validated = $validator->validate(); - - if (method_exists($this, 'extractInputFromRules')) { - // Backwards compatability... - $validated = $this->extractInputFromRules($request, $rules); - } - } catch (ValidationException $exception) { - if (method_exists($this, 'throwValidationException')) { - // Backwards compatability... - $this->throwValidationException($request, $validator); - } else { - $exception->response = $this->buildFailedValidationResponse( - $request, $this->formatValidationErrors($validator) - ); - - throw $exception; - } + if ($validator->fails()) { + $this->throwValidationException($request, $validator); } - return $validated; + return $this->extractInputFromRules($request, $rules); + } + + /** + * Get the request input based on the given validation rules. + * + * @param \Illuminate\Http\Request $request + * @param array $rules + * @return array + */ + protected function extractInputFromRules(Request $request, array $rules) + { + return $request->only(collect($rules)->keys()->map(function ($rule) { + return Str::contains($rule, '.') ? explode('.', $rule)[0] : $rule; + })->unique()->toArray()); + } + + /** + * Throw the failed validation exception. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Validation\Validator $validator + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function throwValidationException(Request $request, $validator) + { + throw new ValidationException($validator, $this->buildFailedValidationResponse( + $request, $this->formatValidationErrors($validator) + )); } /** diff --git a/src/Testing/Concerns/MakesHttpRequests.php b/src/Testing/Concerns/MakesHttpRequests.php index 1dd3bcae..1559e3a8 100644 --- a/src/Testing/Concerns/MakesHttpRequests.php +++ b/src/Testing/Concerns/MakesHttpRequests.php @@ -195,7 +195,7 @@ public function handle(Request $request) * @param array|null $data * @return $this */ - protected function shouldReturnJson(array $data = null) + protected function shouldReturnJson(?array $data = null) { return $this->receiveJson($data); } @@ -239,7 +239,7 @@ public function seeJsonEquals(array $data) * @param bool $negate * @return $this */ - public function seeJson(array $data = null, $negate = false) + public function seeJson(?array $data = null, $negate = false) { if (is_null($data)) { $decodedResponse = json_decode($this->response->getContent(), true); @@ -262,7 +262,7 @@ public function seeJson(array $data = null, $negate = false) * @param array|null $data * @return $this */ - public function dontSeeJson(array $data = null) + public function dontSeeJson(?array $data = null) { return $this->seeJson($data, true); } @@ -274,7 +274,7 @@ public function dontSeeJson(array $data = null) * @param array|null $responseData * @return $this */ - public function seeJsonStructure(array $structure = null, $responseData = null) + public function seeJsonStructure(?array $structure = null, $responseData = null) { $this->response->assertJsonStructure($structure, $responseData); diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index f36c1326..6010cec0 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Console\Kernel; use Illuminate\Support\Facades\Facade; +use Illuminate\View\Component; use Mockery; use PHPUnit\Framework\TestCase as BaseTestCase; @@ -124,6 +125,10 @@ protected function tearDown(): void $this->app->flush(); $this->app = null; } + + Component::flushCache(); + Component::forgetComponentsResolver(); + Component::forgetFactory(); } /** diff --git a/tests/FullApplicationTest.php b/tests/FullApplicationTest.php index d2fae951..61ac37e3 100644 --- a/tests/FullApplicationTest.php +++ b/tests/FullApplicationTest.php @@ -1,13 +1,17 @@ assertNotNull($command); $this->assertEquals('queue:batches-table', $command->getName()); } + + public function testHandlingCommandsTerminatesApplication() + { + $app = new LumenTestApplication(); + $app->register(ConsoleServiceProvider::class); + $app->register(ViewServiceProvider::class); + + $app->instance(ExceptionHandler::class, $mock = m::mock('Laravel\Lumen\Exceptions\Handler[report]')); + $mock->shouldIgnoreMissing(); + + $kernel = $app[Laravel\Lumen\Console\Kernel::class]; + + (fn () => $kernel->getArtisan())->call($kernel)->resolveCommands( + SendEmails::class, + ); + + $terminated = false; + $app->terminating(function () use (&$terminated) { + $terminated = true; + }); + + $input = new ArrayInput(['command' => 'send:emails']); + + $command = $kernel->handle($input, new NullOutput()); + + $this->assertTrue($terminated); + } + + public function testTerminationTests() + { + $app = new LumenTestApplication; + + $result = []; + $callback1 = function () use (&$result) { + $result[] = 1; + }; + + $callback2 = function () use (&$result) { + $result[] = 2; + }; + + $callback3 = function () use (&$result) { + $result[] = 3; + }; + + $app->terminating($callback1); + $app->terminating($callback2); + $app->terminating($callback3); + + $app->terminate(); + + $this->assertEquals([1, 2, 3], $result); + } } class LumenTestService @@ -884,3 +941,13 @@ public function toResponse($request) return $request->route('foo'); } } + +class SendEmails extends Command +{ + protected $signature = 'send:emails'; + + public function handle() + { + // .. + } +} diff --git a/tests/HandleExceptionsTest.php b/tests/HandleExceptionsTest.php index 8d8ab315..bc74efe2 100644 --- a/tests/HandleExceptionsTest.php +++ b/tests/HandleExceptionsTest.php @@ -11,6 +11,10 @@ class HandleExceptionsTest extends TestCase { use RegistersExceptionHandlers; + protected $container; + + protected $config; + protected function setUp(): void { $this->container = new Container;