Skip to content

Commit 59f13dc

Browse files
committed
Fix
1 parent 6fb25c2 commit 59f13dc

File tree

3 files changed

+132
-53
lines changed

3 files changed

+132
-53
lines changed

src/LiveComponent/src/ComponentWithMultiStepFormTrait.php

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@
2424
use function Symfony\Component\String\u;
2525

2626
/**
27+
* Trait for managing multistep forms in Symfony UX LiveComponent.
28+
*
29+
* This trait simplifies the implementation of multistep forms by handling
30+
* step transitions, form validation, data persistence, and state management.
31+
* It provides a structured API for developers to integrate multistep forms
32+
* into their components with minimal boilerplate.
33+
*
2734
* @author Silas Joisten <[email protected]>
2835
* @author Patrick Reimers <[email protected]>
29-
* @author Jules Pietri <[email protected]>
36+
* @author Jules Pietri <[email protected]>
3037
*/
3138
trait ComponentWithMultiStepFormTrait
3239
{
@@ -42,6 +49,9 @@ trait ComponentWithMultiStepFormTrait
4249
#[LiveProp]
4350
public array $stepNames = [];
4451

52+
/**
53+
* Checks if the current form has validation errors.
54+
*/
4555
public function hasValidationErrors(): bool
4656
{
4757
return $this->form->isSubmitted() && !$this->form->isValid();
@@ -50,38 +60,37 @@ public function hasValidationErrors(): bool
5060
/**
5161
* @internal
5262
*
53-
* Must be executed after ComponentWithFormTrait::initializeForm()
63+
* Initializes the form and restores the state from storage.
64+
*
65+
* This method must be executed after `ComponentWithFormTrait::initializeForm()`.
5466
*/
5567
#[PostMount(priority: -250)]
5668
public function initialize(): void
5769
{
58-
$this->currentStepName = $this->getStorage()->get(
59-
\sprintf('%s_current_step_name', self::prefix()),
60-
$this->formView->vars['current_step_name'],
61-
);
70+
$this->currentStepName = $this->getStorage()->get(\sprintf('%s_current_step_name', self::prefix()), $this->formView->vars['current_step_name']);
6271

6372
$this->form = $this->instantiateForm();
6473

65-
$formData = $this->getStorage()->get(\sprintf(
66-
'%s_form_values_%s',
67-
self::prefix(),
68-
$this->currentStepName,
69-
));
74+
$formData = $this->getStorage()->get(\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName));
7075

7176
$this->form->setData($formData);
7277

73-
if ([] === $formData) {
74-
$this->formValues = $this->extractFormValues($this->getFormView());
75-
} else {
76-
$this->formValues = $formData;
77-
}
78+
$this->formValues = [] === $formData
79+
? $this->extractFormValues($this->getFormView())
80+
: $formData;
7881

7982
$this->stepNames = $this->formView->vars['steps_names'];
8083

8184
// Do not move this. The order is important.
8285
$this->formView = null;
8386
}
8487

88+
/**
89+
* Advances to the next step in the form.
90+
*
91+
* Validates the current step, saves its data, and moves to the next step.
92+
* Throws a RuntimeException if no next step is available.
93+
*/
8594
#[LiveAction]
8695
public function next(): void
8796
{
@@ -91,10 +100,7 @@ public function next(): void
91100
return;
92101
}
93102

94-
$this->getStorage()->persist(
95-
\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName),
96-
$this->form->getData(),
97-
);
103+
$this->getStorage()->persist(\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName), $this->form->getData());
98104

99105
$found = false;
100106
$next = null;
@@ -124,23 +130,21 @@ public function next(): void
124130
$this->form = $this->instantiateForm();
125131
$this->formView = null;
126132

127-
$formData = $this->getStorage()->get(\sprintf(
128-
'%s_form_values_%s',
129-
self::prefix(),
130-
$this->currentStepName,
131-
));
133+
$formData = $this->getStorage()->get(\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName));
132134

133-
// I really don't understand why we need to do that. But what I understood is extractFormValues creates
134-
// an array of initial values.
135-
if ([] === $formData) {
136-
$this->formValues = $this->extractFormValues($this->getFormView());
137-
} else {
138-
$this->formValues = $formData;
139-
}
135+
$this->formValues = [] === $formData
136+
? $this->extractFormValues($this->getFormView())
137+
: $formData;
140138

141139
$this->form->setData($formData);
142140
}
143141

142+
/**
143+
* Moves to the previous step in the form.
144+
*
145+
* Retrieves the previous step's data and updates the form state.
146+
* Throws a RuntimeException if no previous step is available.
147+
*/
144148
#[LiveAction]
145149
public function previous(): void
146150
{
@@ -181,18 +185,31 @@ public function previous(): void
181185
$this->form->setData($formData);
182186
}
183187

188+
/**
189+
* Checks if the current step is the first step.
190+
*
191+
* @return bool True if the current step is the first; false otherwise.
192+
*/
184193
#[ExposeInTemplate]
185194
public function isFirst(): bool
186195
{
187196
return $this->currentStepName === $this->stepNames[array_key_first($this->stepNames)];
188197
}
189198

199+
/**
200+
* Checks if the current step is the last step.
201+
*
202+
* @return bool True if the current step is the last; false otherwise.
203+
*/
190204
#[ExposeInTemplate]
191205
public function isLast(): bool
192206
{
193207
return $this->currentStepName === $this->stepNames[array_key_last($this->stepNames)];
194208
}
195209

210+
/**
211+
* Submits the form and triggers the `onSubmit` callback if valid.
212+
*/
196213
#[LiveAction]
197214
public function submit(): void
198215
{
@@ -202,34 +219,35 @@ public function submit(): void
202219
return;
203220
}
204221

205-
$this->getStorage()->persist(
206-
\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName),
207-
$this->form->getData(),
208-
);
222+
$this->getStorage()->persist(\sprintf('%s_form_values_%s', self::prefix(), $this->currentStepName), $this->form->getData());
209223

210224
$this->onSubmit();
211225
}
212226

227+
/**
228+
* Abstract method to be implemented by the component for custom submission logic.
229+
*/
213230
abstract public function onSubmit();
214231

215232
/**
216-
* @return array<string, mixed>
233+
* Retrieves all data from all steps.
234+
*
235+
* @return array<string, mixed> An associative array of step names and their data.
217236
*/
218237
public function getAllData(): array
219238
{
220239
$data = [];
221240

222241
foreach ($this->stepNames as $stepName) {
223-
$data[$stepName] = $this->getStorage()->get(\sprintf(
224-
'%s_form_values_%s',
225-
self::prefix(),
226-
$stepName,
227-
));
242+
$data[$stepName] = $this->getStorage()->get(\sprintf('%s_form_values_%s', self::prefix(), $stepName));
228243
}
229244

230245
return $data;
231246
}
232247

248+
/**
249+
* Resets the form, clearing all stored data and returning to the first step.
250+
*/
233251
public function resetForm(): void
234252
{
235253
foreach ($this->stepNames as $stepName) {
@@ -244,17 +262,33 @@ public function resetForm(): void
244262
$this->formValues = $this->extractFormValues($this->getFormView());
245263
}
246264

265+
/**
266+
* Abstract method to retrieve the storage implementation.
267+
*
268+
* @return StorageInterface The storage instance.
269+
*/
247270
abstract protected function getStorage(): StorageInterface;
248271

249272
/**
250-
* @return class-string<FormInterface>
273+
* Abstract method to specify the form class for the component.
274+
*
275+
* @return class-string<FormInterface> The form class name.
251276
*/
252277
abstract protected static function formClass(): string;
253278

279+
/**
280+
* Abstract method to retrieve the form factory instance.
281+
*
282+
* @return FormFactoryInterface The form factory.
283+
*/
254284
abstract protected function getFormFactory(): FormFactoryInterface;
255285

256286
/**
257287
* @internal
288+
*
289+
* Instantiates the form for the current step.
290+
*
291+
* @return FormInterface The form instance.
258292
*/
259293
protected function instantiateForm(): FormInterface
260294
{
@@ -264,20 +298,18 @@ protected function instantiateForm(): FormInterface
264298
$options['current_step_name'] = $this->currentStepName;
265299
}
266300

267-
return $this->getFormFactory()->create(
268-
type: static::formClass(),
269-
options: $options,
270-
);
301+
return $this->getFormFactory()->create(static::formClass(), null, $options);
271302
}
272303

273304
/**
274305
* @internal
306+
*
307+
* Generates a unique prefix based on the component's class name.
308+
*
309+
* @return string The generated prefix in snake case.
275310
*/
276311
private static function prefix(): string
277312
{
278-
return u(static::class)
279-
->afterLast('\\')
280-
->snake()
281-
->toString();
313+
return u(static::class)->afterLast('\\')->snake()->toString();
282314
}
283315
}

src/LiveComponent/src/Storage/SessionStorage.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@
1616
use Symfony\Component\HttpFoundation\RequestStack;
1717

1818
/**
19+
* Implementation of the StorageInterface using Symfony's session mechanism.
20+
*
21+
* This class provides a session-based storage solution for managing data
22+
* persistence in Symfony UX LiveComponent. It leverages the Symfony
23+
* `RequestStack` to access the session and perform operations such as
24+
* storing, retrieving, and removing data.
25+
*
26+
* Common use cases include persisting component state, such as form data
27+
* or multistep workflow progress, across user interactions.
28+
*
1929
* @author Silas Joisten <[email protected]>
2030
* @author Patrick Reimers <[email protected]>
21-
* @author Jules Pietri <[email protected]>
31+
* @author Jules Pietri <[email protected]>
2232
*/
2333
final class SessionStorage implements StorageInterface
2434
{

src/LiveComponent/src/Storage/StorageInterface.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,52 @@
1414
namespace Symfony\UX\LiveComponent\Storage;
1515

1616
/**
17+
* Interface for a storage mechanism used in Symfony UX LiveComponent.
18+
*
19+
* This interface provides methods for persisting, retrieving, and removing
20+
* data, ensuring a consistent API for managing state across components. It
21+
* is essential for features like multistep forms where data needs to persist
22+
* between user interactions.
23+
*
1724
* @author Silas Joisten <[email protected]>
1825
* @author Patrick Reimers <[email protected]>
19-
* @author Jules Pietri <[email protected]>
26+
* @author Jules Pietri <[email protected]>
2027
*/
2128
interface StorageInterface
2229
{
30+
/**
31+
* Persists a value in the storage using the specified key.
32+
*
33+
* This method is used to save the state of a component or any other
34+
* relevant data that needs to persist across requests or interactions.
35+
*
36+
* @param string $key The unique identifier for the data to store.
37+
* @param mixed $values The value to be stored.
38+
*/
2339
public function persist(string $key, mixed $values): void;
2440

41+
/**
42+
* Removes an entry from the storage based on the specified key.
43+
*
44+
* This method is useful for cleaning up data that is no longer needed,
45+
* such as resetting a form or clearing cached values.
46+
*
47+
* @param string $key The unique identifier for the data to remove.
48+
*/
2549
public function remove(string $key): void;
2650

51+
/**
52+
* Retrieves a value from the storage by its key.
53+
*
54+
* If the specified key does not exist in the storage, this method returns
55+
* a default value instead. This is commonly used to fetch saved state or
56+
* configuration for a component.
57+
*
58+
* @param string $key The unique identifier for the data to retrieve.
59+
* @param mixed $default The default value to return if the key is not found.
60+
* Defaults to an empty array.
61+
*
62+
* @return mixed The value associated with the specified key or the default value.
63+
*/
2764
public function get(string $key, mixed $default = []): mixed;
2865
}

0 commit comments

Comments
 (0)