├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── composer.json ├── config └── laravel-multistep-forms.php ├── phpunit.xml ├── src ├── Form.php ├── Providers │ └── FormServiceProvider.php └── Step.php └── tests ├── TestCase.php ├── Unit └── JsonTest.php └── routes.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.4 5 | - 8.0 6 | 7 | before_script: 8 | - composer self-update 9 | 10 | install: 11 | - composer install --prefer-source --no-interaction --dev 12 | 13 | script: vendor/bin/phpunit 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Nexmo, Inc 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Laravel Multistep Form 2 | 3 | 4 | [![Build Status](https://travis-ci.org/AbdallaMohammed/laravel-multistep-forms.svg?branch=master)](https://travis-ci.org/AbdallaMohammed/laravel-multistep-forms) 5 | 6 | * [Installation](#installation) 7 | * [Example Usage](#example-usage) 8 | * [Steps Usage](#steps-usage) 9 | * [Before](#before-step) 10 | * [After](#after-step) 11 | * [Dynamic](#dynamic-step) 12 | * [Helper Methods](#helper-methods) 13 | 14 | ### Installation 15 | 16 | ```shell script 17 | composer require abdallahmohammed/laravel-multistep-forms 18 | ``` 19 | 20 | ### Example Usage 21 | 22 | ```php 23 | use AbdallaMohammed\Form\Form; 24 | use Illuminate\Support\Facades\Route; 25 | 26 | Route::get('form', function () { 27 | return app(Form::class)->make(function (Form $form) { 28 | // Create a step instance and define rules, messages and attributes 29 | $form->step()->rules([ 30 | 'name' => ['required', 'string'], 31 | ])->messages([ 32 | 'required' => ':attribute Required', 33 | ])->attributes([ 34 | 'name' => 'Name', 35 | ]); 36 | 37 | // Add another step with dynamic rules 38 | $form->step()->dynamicRules(); 39 | }); 40 | })->name('form'); 41 | ``` 42 | 43 | ### Steps Usage 44 | 45 | #### Before Step 46 | 47 | Define a callback to fired **before** a step has been validated. 48 | 49 | > Return a response from this hook to return early before validation occurs. 50 | 51 | `before($step, Closure $closure)` 52 | 53 | > $step could be Step instance, or the number of the step. 54 | 55 | #### After Step 56 | 57 | Define a callback to fired **after** a step has been validated. Step Number or * for all. 58 | 59 | > Return a response from this hook to return early before the form step is incremented. 60 | 61 | `after($step, Closure $closure)` 62 | 63 | > $step could be Step instance, or the number of the step. 64 | 65 | #### Dynamic Step 66 | 67 | You can set a step as dynamic, so the step will take it's **rules**, **messages** and **attributes** from the request. 68 | 69 | For example 70 | 71 | ```php 72 | use AbdallaMohammed\Form\Form; 73 | use Illuminate\Support\Facades\Route; 74 | 75 | Route::get('form', function () { 76 | return app(Form::class)->make(function (Form $form) { 77 | ... 78 | $form->step()->dynamicRules()->messages([ 79 | 'foo' => 'bar', 80 | ]); 81 | ... 82 | }); 83 | })->name('form'); 84 | ``` 85 | 86 | From the example we have defined the **attributes** without the **rules**, so we must send the rules with the request. 87 | Here it is the example of the request body. 88 | 89 | ```json 90 | { 91 | "step": 1, 92 | "1.rules": { 93 | "name": ["required", "string"] 94 | } 95 | } 96 | ``` 97 | 98 | **1.rules** is a reference to first step rules. 99 | 100 | > You can change **1** to the number of the dynamic step. 101 | 102 | As the previous example you can send **1.messages** and **1.attributes** in the request body. 103 | 104 | ### Helper Methods 105 | 106 | #### `stepConfig(?int $step = null)` 107 | 108 | Get the current step config, or a specific step config. 109 | 110 | #### `getValue(string $key, $fallback = null)` 111 | 112 | Get a field value from the form state (session / old input) or fallback to a default. 113 | 114 | #### `setValue(string $key, $value)` 115 | 116 | Set a field value from the session form state. 117 | 118 | #### `currentStep()` 119 | 120 | Get the current saved step number. 121 | 122 | #### `requestedStep()` 123 | 124 | Get the requested step number. 125 | 126 | #### `isStep(int $step = 1)` 127 | 128 | Get the current step number. 129 | 130 | #### `isLastStep()` 131 | 132 | Determine if the current step the last step. 133 | 134 | #### `isPast(int $step, $truthy = true, $falsy = false)` 135 | 136 | Determine if the specified step is in the past. 137 | 138 | #### `isActive(int $step, $truthy = true, $falsy = false)` 139 | 140 | Determine if the specified step is active. 141 | 142 | #### `isNext(int $step, $truthy = true, $falsy = false)` 143 | 144 | Determine if the specified step is in the next. 145 | 146 | 147 | #### `toCollection` 148 | 149 | Get the array representation of the form state as a collection. 150 | 151 | #### `toArray` 152 | 153 | Get the array representation of the form state. 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abdallahmohammed/laravel-multistep-forms", 3 | "description": "Laravel Multistep Forms Builder", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "AbdallahMohammed", 8 | "email": "abdallah.r660@gmail.com" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "require": { 13 | "php": "^7.4|^8.0", 14 | "illuminate/http": "^6.0|^7.0|^8.0", 15 | "illuminate/support": "^6.0|^7.0|^8.0", 16 | "illuminate/session": "^6.0|^7.0|^8.0", 17 | "illuminate/contracts": "^6.0|^7.0|^8.0", 18 | "illuminate/validation": "^6.0|^7.0|^8.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^8.0|^9.0", 22 | "orchestra/testbench": "^5.0|^6.0", 23 | "nunomaduro/larastan": "^0.6" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "AbdallaMohammed\\Form\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "AbdallaMohammed\\Forms\\Tests\\": "tests" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/laravel-multistep-forms.php: -------------------------------------------------------------------------------- 1 | env('SESSION_FORM_DATA_SAVING', true), 5 | 'session_name' => env('FORM_SESSION_NAME', 'multistep-forms'), 6 | ]; 7 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Unit 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Form.php: -------------------------------------------------------------------------------- 1 | namespace = config('laravel-multistep-forms.session_name', 'multistep-forms'); 72 | 73 | $this->request = $request; 74 | $this->session = $session; 75 | 76 | $this->before = new Collection(); 77 | $this->after = new Collection(); 78 | $this->steps = new Collection(); 79 | } 80 | 81 | /** 82 | * Make Form Instance. 83 | * 84 | * @param Closure $callback 85 | * @return Form|\Illuminate\Contracts\Foundation\Application|mixed 86 | */ 87 | public function make(Closure $callback) 88 | { 89 | return $this->tap($callback); 90 | } 91 | 92 | /** 93 | * Add step to form. 94 | * 95 | * @return Step 96 | */ 97 | public function step(): Step 98 | { 99 | $this->steps->put($this->steps->count() + 1, $step = new Step($this->steps->count() + 1, $this->request)); 100 | 101 | return $step; 102 | } 103 | 104 | /** 105 | * Add before step callback. 106 | * 107 | * @param int|string $step 108 | * @param Closure $closure 109 | * @return $this 110 | */ 111 | public function before($step, Closure $closure): self 112 | { 113 | $this->before->put($this->getStepId($step), $closure); 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Add after step callback. 120 | * 121 | * @param int|string $step 122 | * @param Closure $closure 123 | * @return $this 124 | */ 125 | public function after($step, Closure $closure): self 126 | { 127 | $this->after->put($this->getStepId($step), $closure); 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Set the session namespace. 134 | * 135 | * @param string $namespace 136 | * @return $this 137 | */ 138 | public function setNamespace(string $namespace): self 139 | { 140 | $this->namespace = $namespace; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * Set the method namespace. 147 | * 148 | * @param string $method 149 | * @return $this 150 | */ 151 | public function method(string $method): self 152 | { 153 | $this->method = $method; 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get current step. 160 | * 161 | * @return int 162 | */ 163 | public function currentStep(): int 164 | { 165 | return (int) $this->session->get("{$this->namespace}.step", 1); 166 | } 167 | 168 | /** 169 | * Get requested step. 170 | * 171 | * @return int 172 | */ 173 | public function requestedStep(): int 174 | { 175 | return (int) $this->request->get('step', 1); 176 | } 177 | 178 | /** 179 | * Determine the current step. 180 | * 181 | * @param int $step 182 | * @return bool 183 | */ 184 | public function isStep(int $step = 1): bool 185 | { 186 | return $this->currentStep() === $step; 187 | } 188 | 189 | /** 190 | * Get last step number. 191 | * 192 | * @return int 193 | */ 194 | public function lastStep(): int 195 | { 196 | return $this->steps->keys()->filter(function ($value) { 197 | return is_int($value); 198 | })->max() ?? 1; 199 | } 200 | 201 | /** 202 | * Increment the current step to the next.'. 203 | * 204 | * @return $this 205 | */ 206 | protected function nextStep(): self 207 | { 208 | if (! $this->isStep($this->lastStep())) { 209 | $this->setValue('step', $this->requestedStep() + 1); 210 | } 211 | 212 | return $this; 213 | } 214 | 215 | /** 216 | * Get session value. 217 | * 218 | * @param string $key 219 | * @param mixed|null $fallback 220 | * @return mixed 221 | */ 222 | public function getValue(string $key, $fallback = null) 223 | { 224 | return $this->session->get("{$this->namespace}.$key", $this->session->getOldInput($key, $fallback)); 225 | } 226 | 227 | /** 228 | * Set session value. 229 | * 230 | * @param string $key 231 | * @param mixed $value 232 | * @return $this 233 | */ 234 | public function setValue(string $key, $value): self 235 | { 236 | $this->session->put("{$this->namespace}.$key", $value); 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Is the current step the last? 243 | * 244 | * @return bool 245 | */ 246 | public function isLastStep(): bool 247 | { 248 | return $this->isStep($this->lastStep()); 249 | } 250 | 251 | /** 252 | * @param int $step 253 | * @param mixed|null $active 254 | * @param mixed|null $fallback 255 | * @return mixed 256 | */ 257 | public function isActive(int $step, $active = true, $fallback = false) 258 | { 259 | if ($this->isStep($step)) { 260 | return $active; 261 | } 262 | 263 | return $fallback; 264 | } 265 | 266 | /** 267 | * @param int $step 268 | * @param mixed $active 269 | * @param mixed $fallback 270 | * @return mixed 271 | */ 272 | public function isPrev(int $step, $active = true, $fallback = false) 273 | { 274 | if ($this->steps->has($step) && $this->currentStep() > $step) { 275 | return $active; 276 | } 277 | 278 | return $fallback; 279 | } 280 | 281 | /** 282 | * @param int $step 283 | * @param mixed|null $active 284 | * @param mixed|null $fallback 285 | * @return mixed 286 | */ 287 | public function isNext(int $step, $active = true, $fallback = false) 288 | { 289 | if ($this->steps->has($step) && $this->currentStep() < $step) { 290 | return $active; 291 | } 292 | 293 | return $fallback; 294 | } 295 | 296 | /** 297 | * Get the instance as an array. 298 | * 299 | * @return array 300 | */ 301 | public function toArray(): array 302 | { 303 | return $this->session->get($this->namespace, []); 304 | } 305 | 306 | /** 307 | * Get the instance as an Collection. 308 | * 309 | * @return Collection 310 | */ 311 | public function toCollection(): Collection 312 | { 313 | return Collection::make($this->toArray()); 314 | } 315 | 316 | /** 317 | * Create an HTTP response that represents the object. 318 | * 319 | * @param \Illuminate\Http\Request|null $request 320 | * @return \Illuminate\Http\Response 321 | */ 322 | public function toResponse($request = null) 323 | { 324 | $this->request = ($request ?? $this->request); 325 | 326 | return $this->handleRequest(); 327 | } 328 | 329 | /** 330 | * Get the current step config or by number. 331 | * 332 | * @param int|null $step 333 | * @return Step 334 | */ 335 | public function stepConfig(?int $step = null): Step 336 | { 337 | return $this->steps->get($step ?? $this->currentStep()); 338 | } 339 | 340 | /** 341 | * @param mixed ...$params 342 | */ 343 | public function useView(...$params) 344 | { 345 | $this->view = func_get_arg(0); 346 | $this->data = array_merge($this->data, is_array(func_get_arg(1)) ? func_get_arg(1) : []); 347 | 348 | return $this; 349 | } 350 | 351 | /** 352 | * @param array $data 353 | * @return $this 354 | */ 355 | public function mergeData(array $data) 356 | { 357 | $this->data = array_merge($data, $this->data); 358 | 359 | return $this; 360 | } 361 | 362 | /** 363 | * @param mixed $condition 364 | * @param Closure $callback 365 | * @return $this 366 | */ 367 | public function mergeDataWhen($condition, Closure $callback) 368 | { 369 | $data = []; 370 | if ((is_integer($condition) && $this->isStep($this->getStepId($condition))) 371 | || (is_bool($condition) && $condition === true) 372 | || (is_callable($condition) && is_bool(value($condition($this))) && value($condition($this)) === true)) { 373 | $data = value($callback($this)); 374 | } 375 | 376 | $this->data = array_merge($data, $this->data); 377 | 378 | return $this; 379 | } 380 | 381 | 382 | /** 383 | * Tap into instance (invokable classes). 384 | * 385 | * @param Closure|mixed $closure 386 | * @return $this 387 | */ 388 | public function tap(Closure $closure): self 389 | { 390 | $closure($this); 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * @param int $stepId 397 | * @return Step 398 | */ 399 | public function getStepInstance(int $stepId): Step 400 | { 401 | return $this->steps->filter(function ($step) use ($stepId) { 402 | return $step->getId() == $stepId; 403 | })->first(); 404 | } 405 | 406 | /** 407 | * Handle the validated request. 408 | * 409 | * @return mixed 410 | */ 411 | protected function handleRequest() 412 | { 413 | $this->setupSession(); 414 | 415 | if ($this->request->isMethod($this->method)) { 416 | if ($response = ( 417 | $this->handleBefore('*') ?? 418 | $this->handleBefore($this->requestedStep()) 419 | )) { 420 | return $response; 421 | } 422 | 423 | $this->save($this->validate()); 424 | 425 | if ($response = ( 426 | $this->handleAfter('*') ?? 427 | $this->handleAfter($this->currentStep()) 428 | )) { 429 | return $response; 430 | } 431 | 432 | $this->nextStep(); 433 | } 434 | 435 | return $this->renderResponse(); 436 | } 437 | 438 | /** 439 | * Setup the session if it hasn't been started. 440 | * 441 | * @return void 442 | */ 443 | protected function setupSession(): void 444 | { 445 | if (! is_numeric($this->getValue('step', false)) && config('laravel-mutlistep-forms.enable_session', true)) { 446 | $this->setValue('step', 1); 447 | } 448 | } 449 | 450 | /** 451 | * Render the request as a response. 452 | * 453 | * @return JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View 454 | */ 455 | protected function renderResponse() 456 | { 457 | if (! $this->usesViews() || $this->needsJsonResponse()) { 458 | return new JsonResponse((object) [ 459 | 'form' => $this->toArray(), 460 | ]); 461 | } 462 | 463 | if (! $this->request->isMethod('GET')) { 464 | return redirect()->back(); 465 | } 466 | 467 | return View::make($this->view, array_merge([ 468 | 'form' => $this, 469 | ], $this->data)); 470 | } 471 | 472 | /** 473 | * Request needs JSON response. 474 | * 475 | * @return bool 476 | */ 477 | protected function needsJsonResponse(): bool 478 | { 479 | return $this->request->wantsJson() || $this->request->isXmlHttpRequest(); 480 | } 481 | 482 | /** 483 | * @return bool 484 | */ 485 | protected function usesViews(): bool 486 | { 487 | return ! empty($this->view) && is_string($this->view); 488 | } 489 | 490 | /** 491 | * Save the validation data to the session. 492 | * 493 | * @param array $data 494 | * @return $this 495 | */ 496 | protected function save(array $data = []): self 497 | { 498 | if (config('laravel-multistep-forms.enable_session', true)) { 499 | $this->session->put($this->namespace, array_merge( 500 | $this->session->get($this->namespace, []), 501 | $data 502 | )); 503 | } 504 | 505 | return $this; 506 | } 507 | 508 | /** 509 | * Validate the request. 510 | * 511 | * @return array 512 | */ 513 | protected function validate(): array 514 | { 515 | $step = $this->stepConfig($this->requestedStep()); 516 | 517 | return $this->request->validate( 518 | array_merge($step->getRules(), [ 519 | 'step' => ['required', 'numeric', Rule::in(range(1, $this->lastStep()))], 520 | ]), 521 | $step->getMessages(), 522 | $step->getAttributes() 523 | ); 524 | } 525 | 526 | /** 527 | * Handle "Before" Callback. 528 | * 529 | * @param int|string $key 530 | * @return mixed 531 | */ 532 | protected function handleBefore($key) 533 | { 534 | if ($callback = $this->before->get($key)) { 535 | return $callback($this, $this->steps->get($key)); 536 | } 537 | } 538 | 539 | /** 540 | * Handle "After" Callback. 541 | * 542 | * @param int|string $key 543 | * @return mixed 544 | */ 545 | protected function handleAfter($key) 546 | { 547 | if ($callback = $this->after->get($key)) { 548 | return $callback($this, $this->steps->get($key)); 549 | } 550 | } 551 | 552 | /** 553 | * @param $step 554 | * @return int 555 | */ 556 | protected function getStepId($step): int 557 | { 558 | return (is_object($step) && $step instanceof Step) ? $step->getId() : $step; 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/Providers/FormServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Form::class); 18 | } 19 | 20 | /** 21 | * Bootstrap services. 22 | * 23 | * @return void 24 | */ 25 | public function boot() 26 | { 27 | $dist = __DIR__.'/../../config/laravel-multistep-forms.php'; 28 | 29 | // If we're installing in to a Lumen project, config_path 30 | // won't exist so we can't auto-publish the config 31 | if (function_exists('config_path')) { 32 | // Publishes config File. 33 | $this->publishes([ 34 | $dist => config_path('laravel-multistep-forms.php'), 35 | ]); 36 | } 37 | 38 | // Merge config. 39 | $this->mergeConfigFrom($dist, 'laravel-multistep-forms'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Step.php: -------------------------------------------------------------------------------- 1 | id = $id; 48 | $this->request = $request; 49 | } 50 | 51 | /** 52 | * @param array $rules 53 | * @return $this 54 | */ 55 | public function rules(array $rules = []): self 56 | { 57 | $this->rules = $rules; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @param array $messages 64 | * @return $this 65 | */ 66 | public function messages(array $messages = []): self 67 | { 68 | $this->messages = $messages; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param array $attributes 75 | * @return $this 76 | */ 77 | public function attributes(array $attributes = []): self 78 | { 79 | $this->attributes = $attributes; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param bool $value 86 | * @return $this 87 | */ 88 | public function dynamicRules($value = true): self 89 | { 90 | $this->dynamicRules = $value; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return bool 97 | */ 98 | public function isDynamicRules(): bool 99 | { 100 | return $this->dynamicRules; 101 | } 102 | 103 | /** 104 | * @return int 105 | */ 106 | public function getId(): int 107 | { 108 | return $this->id; 109 | } 110 | 111 | /** 112 | * @return array 113 | */ 114 | public function getRules(): array 115 | { 116 | if ($this->isDynamicRules() && ! empty($rules = $this->request->get("{$this->id}.rules"))) { 117 | return $rules; 118 | } 119 | 120 | return $this->rules; 121 | } 122 | 123 | /** 124 | * @return array 125 | */ 126 | public function getMessages(): array 127 | { 128 | if ($this->isDynamicRules() && ! empty($messages = $this->request->get("{$this->id}.messages"))) { 129 | return $messages; 130 | } 131 | 132 | return $this->messages; 133 | } 134 | 135 | /** 136 | * @return array 137 | */ 138 | public function getAttributes(): array 139 | { 140 | if ($this->isDynamicRules() && ! empty($attributes = $this->request->get("{$this->id}.attributes"))) { 141 | return $attributes; 142 | } 143 | 144 | return $this->attributes; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app['config']->set('app.debug', true); 42 | $this->app['config']->set('app.key', Str::random(32)); 43 | 44 | require __DIR__.'/routes.php'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/JsonTest.php: -------------------------------------------------------------------------------- 1 | json('POST', route('form'), []) 13 | ->assertJsonValidationErrors([ 14 | 'name', 'step', 15 | ]); 16 | } 17 | 18 | /** @test */ 19 | public function test_dynamic_rules() 20 | { 21 | $this->json('POST', route('form'), [ 22 | '2.rules' => [ 23 | 'name' => ['required'], 24 | ], 25 | ]) 26 | ->assertJsonValidationErrors([ 27 | 'name', 28 | ]); 29 | } 30 | 31 | /** @test */ 32 | public function test_form_session_data() 33 | { 34 | $this->json('POST', route('form'), [ 35 | 'step' => 1, 36 | ]) 37 | ->assertSessionHas('test.step', 1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/routes.php: -------------------------------------------------------------------------------- 1 | make(function ($form) { 8 | $form->step()->rules([ 9 | 'name' => ['required', 'string'], 10 | ]); 11 | 12 | $form->step()->dynamicRules(); 13 | })->setNamespace('test'); 14 | })->middleware('web')->name('form'); 15 | --------------------------------------------------------------------------------