From 6cad8e43c313a01282336111fe15053caad75db4 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Thu, 5 Sep 2024 00:34:12 +0100 Subject: [PATCH 1/4] [feature/validation-wrapping] Refactor: Move ValidationHandler files to appropriate directories Relocated ValidationHandler test and documentation files to better align with directory structure. This change improves maintainability and helps in organizing related files together consistently. --- docs/{Handlers => Validation}/ValidationHandler.md | 0 tests/{Handlers => Validation}/ValidationHandlerTest.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{Handlers => Validation}/ValidationHandler.md (100%) rename tests/{Handlers => Validation}/ValidationHandlerTest.php (100%) diff --git a/docs/Handlers/ValidationHandler.md b/docs/Validation/ValidationHandler.md similarity index 100% rename from docs/Handlers/ValidationHandler.md rename to docs/Validation/ValidationHandler.md diff --git a/tests/Handlers/ValidationHandlerTest.php b/tests/Validation/ValidationHandlerTest.php similarity index 100% rename from tests/Handlers/ValidationHandlerTest.php rename to tests/Validation/ValidationHandlerTest.php From 8a665b6013189040c372adda7713d4f0dec17e4f Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Thu, 5 Sep 2024 00:34:22 +0100 Subject: [PATCH 2/4] [feature/validation-wrapping] Add ValidationExceptionBuilder class with tests and documentation Introduces `ValidationExceptionBuilder` for streamlined validation exception handling in Laravel. The update includes comprehensive documentation and unit tests to ensure robustness and ease of use. --- docs/Validation/ValidationExceptionBuilder.md | 146 ++++++++++ src/Validation/ValidationExceptionBuilder.php | 260 ++++++++++++++++++ .../ValidationExceptionBuilderTest.php | 171 ++++++++++++ 3 files changed, 577 insertions(+) create mode 100644 docs/Validation/ValidationExceptionBuilder.md create mode 100644 src/Validation/ValidationExceptionBuilder.php create mode 100644 tests/Validation/ValidationExceptionBuilderTest.php diff --git a/docs/Validation/ValidationExceptionBuilder.md b/docs/Validation/ValidationExceptionBuilder.md new file mode 100644 index 0000000..a1eb7d6 --- /dev/null +++ b/docs/Validation/ValidationExceptionBuilder.md @@ -0,0 +1,146 @@ +# ValidationExceptionBuilder + +`ValidationExceptionBuilder` is a fluent interface for building and throwing validation exceptions in Laravel +applications. It provides a convenient way to create custom validation exceptions with redirects, error messages, and +additional parameters. + +## Basic Usage + +Here's a quick example of how to use the `ValidationExceptionBuilder`: + +```php +use Midnite81\Core\Validation\ValidationExceptionBuilder; + +ValidationExceptionBuilder::message('Invalid input') + ->redirectTo('/form') + ->flash('Please correct the errors and try again.') + ->throwException(); +``` + +This will throw a `ValidationException` with the message "Invalid input", redirect the user to '/form', and flash a +message to the session. + +## API Reference + +### Static Methods + +#### `message(string $message): self` + +Create a new ValidationExceptionBuilder instance with the specified error message. + +### Instance Methods + +#### `redirectTo(string $url): self` + +Set the URL to redirect to after validation failure. + +#### `redirectBack(): self` + +Set the redirect to go back to the previous URL. + +#### `redirectRoute(string $name, array $parameters = []): self` + +Set a named route to redirect to after validation failure. + +#### `fragment(string $fragment): self` + +Set the URL fragment (hash) to append to the redirect URL. + +#### `withQueryParameters(array $params): self` + +Set query parameters to append to the redirect URL. + +#### `errorBag(string $errorBag): self` + +Set the error bag name for the validation exception. + +#### `flash(?string $message = null, string $key = 'error'): self` + +Enable session flashing with an optional custom message and key. + +#### `withException(string $exceptionClass): self` + +Set the exception class to be thrown. + +#### `withExceptionCallback(callable $callback): self` + +Set a callback for creating the exception. + +#### `throwException(): void` + +Throw the configured exception. + +#### `throwExceptionIf($condition): void` + +Throw the configured exception if the given condition is true. + +#### `throwExceptionUnless($condition): void` + +Throw the configured exception unless the given condition is true. + +## Examples + +### Basic Validation Exception + +```php +ValidationExceptionBuilder::message('The email is invalid') + ->redirectBack() + ->throwException(); +``` + +### Custom Redirect with Query Parameters + +```php +ValidationExceptionBuilder::message('Invalid input') + ->redirectTo('/users') + ->withQueryParameters(['sort' => 'name', 'order' => 'asc']) + ->throwException(); +``` + +### Using Named Routes + +```php +ValidationExceptionBuilder::message('Access denied') + ->redirectRoute('dashboard', ['user' => $userId]) + ->throwException(); +``` + +### Flashing Messages + +```php +ValidationExceptionBuilder::message('Form submission failed') + ->redirectBack() + ->flash('Please correct the errors and try again.', 'warning') + ->throwException(); +``` + +### Conditional Exception Throwing + +```php +$someCondition = true; + +ValidationExceptionBuilder::message('Conditional error') + ->redirectBack() + ->throwExceptionIf($someCondition); +``` + +### Custom Exception Class + +```php +class MyCustomException extends Exception {} + +ValidationExceptionBuilder::message('Something went wrong') + ->withException(MyCustomException::class) + ->throwException(); +``` + +### Using Exception Callback + +```php +ValidationExceptionBuilder::message('Custom handling required') + ->withExceptionCallback(function ($message, $url, $errorBag) { + // Custom logic here + return new MyCustomException($message); + }) + ->throwException(); +``` diff --git a/src/Validation/ValidationExceptionBuilder.php b/src/Validation/ValidationExceptionBuilder.php new file mode 100644 index 0000000..0ed53e5 --- /dev/null +++ b/src/Validation/ValidationExceptionBuilder.php @@ -0,0 +1,260 @@ +message = $message; + $this->redirectUrl = URL::previous(); + } + + /** + * Create a new ValidationExceptionBuilder instance with the specified error message. + * + * @param string $message The validation error message. + * @return self + */ + public static function message(string $message): self + { + return new self($message); + } + + /** + * Set the URL to redirect to after validation failure. + * + * @param string $url The URL to redirect to. + * @return self + */ + public function redirectTo(string $url): self + { + $this->redirectUrl = $url; + $this->routeName = null; // Reset route if URL is set directly + return $this; + } + + /** + * Set the redirect to go back to the previous URL. + * + * @return self + */ + public function redirectBack(): self + { + $this->redirectUrl = URL::previous(); + $this->routeName = null; // Reset route if redirecting back + return $this; + } + + /** + * Set a named route to redirect to after validation failure. + * + * @param string $name The name of the route. + * @param array $parameters The parameters for the route. + * @return self + */ + public function redirectRoute(string $name, array $parameters = []): self + { + $this->routeName = $name; + $this->routeParameters = $parameters; + return $this; + } + + /** + * Set the URL fragment (hash) to append to the redirect URL. + * + * @param string $fragment The URL fragment. + * @return self + */ + public function fragment(string $fragment): self + { + $this->fragment = $fragment; + return $this; + } + + /** + * Set query parameters to append to the redirect URL. + * + * @param array $params An associative array of query parameters. + * @return self + */ + public function withQueryParameters(array $params): self + { + $this->queryParameters = $params; + return $this; + } + + /** + * Set the error bag name for the validation exception. + * + * @param string $errorBag The name of the error bag. + * @return self + */ + public function errorBag(string $errorBag): self + { + $this->errorBag = $errorBag; + return $this; + } + + /** + * Enable session flashing with an optional custom message and key. + * + * @param string|null $message The message to flash (defaults to the validation message if null). + * @param string $key The key to use for flashing (defaults to 'error'). + * @return self + */ + public function flash(?string $message = null, string $key = 'error'): self + { + $this->shouldFlash = true; + $this->flashMessage = $message; + $this->flashKey = $key; + return $this; + } + + /** + * Set the exception class to be thrown. + * + * @param string $exceptionClass The fully qualified class name of the exception. + * @return self + * @throws \InvalidArgumentException If the class is not a subclass of Exception. + */ + public function withException(string $exceptionClass): self + { + if (!is_subclass_of($exceptionClass, Exception::class)) { + throw new \InvalidArgumentException("The provided class must be a subclass of Exception."); + } + $this->exceptionClass = $exceptionClass; + return $this; + } + + /** + * Set a callback for creating the exception. + * + * @param callable $callback The callback function for creating the exception. + * @return self + */ + public function withExceptionCallback(callable $callback): self + { + $this->exceptionCallback = $callback; + return $this; + } + + /** + * Throw the configured exception. + * + * @throws Exception + */ + public function throwException(): void + { + $url = $this->buildUrl(); + + if ($this->exceptionCallback) { + $exception = call_user_func($this->exceptionCallback, $this->message, $url, $this->errorBag); + } else { + $exception = $this->createDefaultException($url); + } + + if ($this->shouldFlash) { + Session::flash($this->flashKey, $this->flashMessage ?? $this->message); + } + + throw $exception; + } + + /** + * Throw the configured exception if the given condition is true. + * + * @param bool|callable $condition A boolean value or a callback that returns a boolean. + * @throws Exception + */ + public function throwExceptionIf($condition): void + { + $shouldThrow = is_callable($condition) ? $condition() : $condition; + + if ($shouldThrow) { + $this->throwException(); + } + } + + /** + * Throw the configured exception unless the given condition is true. + * + * @param bool|callable $condition A boolean value or a callback that returns a boolean. + * @throws Exception + */ + public function throwExceptionUnless($condition): void + { + $shouldNotThrow = is_callable($condition) ? $condition() : $condition; + + if (!$shouldNotThrow) { + $this->throwException(); + } + } + + /** + * Build the complete redirect URL with query parameters and fragment. + * + * @return string The complete URL. + */ + protected function buildUrl(): string + { + if ($this->routeName) { + $url = Route::urlTo($this->routeName, $this->routeParameters); + } else { + $url = $this->redirectUrl; + } + + if (!empty($this->queryParameters)) { + $url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . http_build_query($this->queryParameters); + } + + if ($this->fragment) { + $url .= '#' . $this->fragment; + } + + return $url; + } + + /** + * Create the default exception based on the configured exception class. + * + * @param string $url The redirect URL. + * @return Exception + */ + protected function createDefaultException(string $url): Exception + { + if ($this->exceptionClass === ValidationException::class) { + $exception = ValidationException::withMessages(['message' => $this->message])->redirectTo($url); + if ($this->errorBag) { + $exception->errorBag($this->errorBag); + } + return $exception; + } + + return new $this->exceptionClass($this->message); + } +} diff --git a/tests/Validation/ValidationExceptionBuilderTest.php b/tests/Validation/ValidationExceptionBuilderTest.php new file mode 100644 index 0000000..2f25a6a --- /dev/null +++ b/tests/Validation/ValidationExceptionBuilderTest.php @@ -0,0 +1,171 @@ +andReturn('http://example.com/previous'); +}); + +test('it creates a new instance with a message', function () { + $builder = ValidationExceptionBuilder::message('Test message'); + expect($builder)->toBeInstanceOf(ValidationExceptionBuilder::class); +}); + +test('it sets redirect URL', function () { + $builder = ValidationExceptionBuilder::message('Test') + ->redirectTo('http://example.com/redirect'); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->redirectTo)->toBe('http://example.com/redirect'); +}); + +test('it redirects back to previous URL', function () { + $builder = ValidationExceptionBuilder::message('Test')->redirectBack(); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->redirectTo)->toBe('http://example.com/previous'); +}); + +test('it sets redirect route', function () { + Route::shouldReceive('urlTo') + ->with('test.route', ['id' => 1]) + ->andReturn('http://example.com/test/1'); + + $builder = ValidationExceptionBuilder::message('Test') + ->redirectRoute('test.route', ['id' => 1]); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->redirectTo)->toBe('http://example.com/test/1'); +}); + +test('it adds fragment to redirect URL', function () { + $builder = ValidationExceptionBuilder::message('Test') + ->redirectTo('http://example.com/redirect') + ->fragment('section1'); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->redirectTo)->toBe('http://example.com/redirect#section1'); +}); + +test('it adds query parameters to redirect URL', function () { + $builder = ValidationExceptionBuilder::message('Test') + ->redirectTo('http://example.com/redirect') + ->withQueryParameters(['param1' => 'value1', 'param2' => 'value2']); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->redirectTo)->toBe('http://example.com/redirect?param1=value1¶m2=value2'); +}); + +test('it sets error bag', function () { + $builder = ValidationExceptionBuilder::message('Test') + ->errorBag('custom_error_bag'); + + $exception = null; + try { + $builder->throwException(); + } catch (ValidationException $e) { + $exception = $e; + } + + expect($exception->errorBag)->toBe('custom_error_bag'); +}); + +test('it flashes message to session', function () { + Session::shouldReceive('flash')->once()->with('error', 'Test message'); + + $builder = ValidationExceptionBuilder::message('Test message') + ->flash(); + + try { + $builder->throwException(); + } catch (ValidationException $e) { + // Exception thrown as expected + } +}); + +test('it flashes custom message with custom key', function () { + Session::shouldReceive('flash')->once()->with('custom_key', 'Custom flash message'); + + $builder = ValidationExceptionBuilder::message('Test message') + ->flash('Custom flash message', 'custom_key'); + + try { + $builder->throwException(); + } catch (ValidationException $e) { + // Exception thrown as expected + } +}); + +test('it throws custom exception', function () { + $builder = ValidationExceptionBuilder::message('Test message') + ->withException(\RuntimeException::class); + + expect(fn() => $builder->throwException())->toThrow(\RuntimeException::class, 'Test message'); +}); + +test('it uses custom exception callback', function () { + $builder = ValidationExceptionBuilder::message('Test message') + ->withExceptionCallback(function ($message, $url, $errorBag) { + return new \RuntimeException("Custom: $message"); + }); + + expect(fn() => $builder->throwException())->toThrow(\RuntimeException::class, 'Custom: Test message'); +}); + +test('it throws exception conditionally with if', function () { + $builder = ValidationExceptionBuilder::message('Test message'); + + expect(fn() => $builder->throwExceptionIf(true))->toThrow(ValidationException::class); + expect(fn() => $builder->throwExceptionIf(false))->not->toThrow(ValidationException::class); + + expect(fn() => $builder->throwExceptionIf(fn() => true))->toThrow(ValidationException::class); + expect(fn() => $builder->throwExceptionIf(fn() => false))->not->toThrow(ValidationException::class); +}); + +test('it throws exception conditionally with unless', function () { + $builder = ValidationExceptionBuilder::message('Test message'); + + expect(fn() => $builder->throwExceptionUnless(false))->toThrow(ValidationException::class); + expect(fn() => $builder->throwExceptionUnless(true))->not->toThrow(ValidationException::class); + + expect(fn() => $builder->throwExceptionUnless(fn() => false))->toThrow(ValidationException::class); + expect(fn() => $builder->throwExceptionUnless(fn() => true))->not->toThrow(ValidationException::class); +}); From f562f1024ff043cb5001875ca5c5db08b40edcfb Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Thu, 5 Sep 2024 00:37:58 +0100 Subject: [PATCH 3/4] [feature/validation-wrapping] Refactor ValidationExceptionBuilder for readability Added newline separators in ValidationExceptionBuilder class properties and methods for improved readability. Modified test cases to include spaces in callback expectations for consistency. --- src/Validation/ValidationExceptionBuilder.php | 26 ++++++++++++++++++- .../ValidationExceptionBuilderTest.php | 20 +++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Validation/ValidationExceptionBuilder.php b/src/Validation/ValidationExceptionBuilder.php index 0ed53e5..58bcabc 100644 --- a/src/Validation/ValidationExceptionBuilder.php +++ b/src/Validation/ValidationExceptionBuilder.php @@ -11,16 +11,27 @@ class ValidationExceptionBuilder { protected string $message; + protected string $redirectUrl; + protected ?string $fragment = null; + protected array $queryParameters = []; + protected ?string $routeName = null; + protected array $routeParameters = []; + protected ?string $errorBag = null; + protected bool $shouldFlash = false; + protected string $flashKey = 'error'; + protected ?string $flashMessage = null; + protected string $exceptionClass = ValidationException::class; + protected mixed $exceptionCallback = null; /** @@ -55,6 +66,7 @@ public function redirectTo(string $url): self { $this->redirectUrl = $url; $this->routeName = null; // Reset route if URL is set directly + return $this; } @@ -67,6 +79,7 @@ public function redirectBack(): self { $this->redirectUrl = URL::previous(); $this->routeName = null; // Reset route if redirecting back + return $this; } @@ -81,6 +94,7 @@ public function redirectRoute(string $name, array $parameters = []): self { $this->routeName = $name; $this->routeParameters = $parameters; + return $this; } @@ -93,6 +107,7 @@ public function redirectRoute(string $name, array $parameters = []): self public function fragment(string $fragment): self { $this->fragment = $fragment; + return $this; } @@ -105,6 +120,7 @@ public function fragment(string $fragment): self public function withQueryParameters(array $params): self { $this->queryParameters = $params; + return $this; } @@ -117,6 +133,7 @@ public function withQueryParameters(array $params): self public function errorBag(string $errorBag): self { $this->errorBag = $errorBag; + return $this; } @@ -132,6 +149,7 @@ public function flash(?string $message = null, string $key = 'error'): self $this->shouldFlash = true; $this->flashMessage = $message; $this->flashKey = $key; + return $this; } @@ -140,14 +158,16 @@ public function flash(?string $message = null, string $key = 'error'): self * * @param string $exceptionClass The fully qualified class name of the exception. * @return self + * * @throws \InvalidArgumentException If the class is not a subclass of Exception. */ public function withException(string $exceptionClass): self { if (!is_subclass_of($exceptionClass, Exception::class)) { - throw new \InvalidArgumentException("The provided class must be a subclass of Exception."); + throw new \InvalidArgumentException('The provided class must be a subclass of Exception.'); } $this->exceptionClass = $exceptionClass; + return $this; } @@ -160,6 +180,7 @@ public function withException(string $exceptionClass): self public function withExceptionCallback(callable $callback): self { $this->exceptionCallback = $callback; + return $this; } @@ -189,6 +210,7 @@ public function throwException(): void * Throw the configured exception if the given condition is true. * * @param bool|callable $condition A boolean value or a callback that returns a boolean. + * * @throws Exception */ public function throwExceptionIf($condition): void @@ -204,6 +226,7 @@ public function throwExceptionIf($condition): void * Throw the configured exception unless the given condition is true. * * @param bool|callable $condition A boolean value or a callback that returns a boolean. + * * @throws Exception */ public function throwExceptionUnless($condition): void @@ -252,6 +275,7 @@ protected function createDefaultException(string $url): Exception if ($this->errorBag) { $exception->errorBag($this->errorBag); } + return $exception; } diff --git a/tests/Validation/ValidationExceptionBuilderTest.php b/tests/Validation/ValidationExceptionBuilderTest.php index 2f25a6a..6e58f76 100644 --- a/tests/Validation/ValidationExceptionBuilderTest.php +++ b/tests/Validation/ValidationExceptionBuilderTest.php @@ -138,7 +138,7 @@ $builder = ValidationExceptionBuilder::message('Test message') ->withException(\RuntimeException::class); - expect(fn() => $builder->throwException())->toThrow(\RuntimeException::class, 'Test message'); + expect(fn () => $builder->throwException())->toThrow(\RuntimeException::class, 'Test message'); }); test('it uses custom exception callback', function () { @@ -147,25 +147,25 @@ return new \RuntimeException("Custom: $message"); }); - expect(fn() => $builder->throwException())->toThrow(\RuntimeException::class, 'Custom: Test message'); + expect(fn () => $builder->throwException())->toThrow(\RuntimeException::class, 'Custom: Test message'); }); test('it throws exception conditionally with if', function () { $builder = ValidationExceptionBuilder::message('Test message'); - expect(fn() => $builder->throwExceptionIf(true))->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionIf(false))->not->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionIf(true))->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionIf(false))->not->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionIf(fn() => true))->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionIf(fn() => false))->not->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionIf(fn () => true))->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionIf(fn () => false))->not->toThrow(ValidationException::class); }); test('it throws exception conditionally with unless', function () { $builder = ValidationExceptionBuilder::message('Test message'); - expect(fn() => $builder->throwExceptionUnless(false))->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionUnless(true))->not->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionUnless(false))->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionUnless(true))->not->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionUnless(fn() => false))->toThrow(ValidationException::class); - expect(fn() => $builder->throwExceptionUnless(fn() => true))->not->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionUnless(fn () => false))->toThrow(ValidationException::class); + expect(fn () => $builder->throwExceptionUnless(fn () => true))->not->toThrow(ValidationException::class); }); From b239d7ce2a3e31dd36ac887ef4b35b050c3aa781 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Thu, 5 Sep 2024 00:56:39 +0100 Subject: [PATCH 4/4] [feature/validation-wrapping] Reorganize validation-related documentation Moved ValidationHandler under a new 'Validation' section for consistency. Added the new entry for ValidationExceptionBuilder. This helps in better grouping and locating validation-related resources. --- readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 8d2b637..7449be1 100644 --- a/readme.md +++ b/readme.md @@ -16,8 +16,6 @@ This package contains; - [Eloquent Helpers](docs/EloquentHelpers.md) - [Entities, Requests and Responses](docs/Entities_Requests_Responses.md) - [Exceptions](docs/Exceptions.md) -- Handlers - - [ValidationHandler](docs/Handlers/ValidationHandler.md) - [Helper Functions](docs/HelperFunctions.md) - [first](docs/HelperFunctions.md#first-value) - [uuid](docs/HelperFunctions.md#uuid) @@ -38,6 +36,9 @@ This package contains; - Transformers - [FileLimiter](docs/Transformers/FileLimiter.md) - [HumanReadableNumber](docs/Transformers/HumanReadableNumber.md) +- Validation + - [ValidationHandler](docs/Handlers/ValidationHandler.md) + - [ValidationExceptionBuilder](docs/Validation/ValidationExceptionBuilder.md)) ## Installation