-
-
Notifications
You must be signed in to change notification settings - Fork 14
[#610] Added 'StateTrait' with Drupal State API management steps #621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cb4cec4
5236d15
9486985
6bdb6ab
d4d8d64
cb7a321
7bd11bc
1604909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace DrevOps\BehatSteps\Drupal; | ||
|
|
||
| use Behat\Behat\Hook\Scope\AfterScenarioScope; | ||
| use Behat\Behat\Hook\Scope\BeforeScenarioScope; | ||
| use Behat\Gherkin\Node\TableNode; | ||
| use Behat\Hook\AfterScenario; | ||
| use Behat\Hook\BeforeScenario; | ||
| use Behat\Step\Given; | ||
| use Behat\Step\Then; | ||
|
|
||
| /** | ||
| * Manage and assert Drupal State API values with automatic revert. | ||
| * | ||
| * Provides set, delete, and assertion steps for keys stored through | ||
| * `\Drupal::state()`. Touched keys are snapshotted on first access and | ||
| * reverted after the scenario finishes. | ||
| * | ||
| * Skip the revert with `@behat-steps-skip:stateAfterScenario` or with the | ||
| * convenience tag `@behat-steps-skip:StateTrait`. The snapshot registry is | ||
| * cleared unconditionally before and after the scenario to prevent state | ||
| * leaking into subsequent scenarios. | ||
| */ | ||
| trait StateTrait { | ||
|
|
||
| /** | ||
| * Original state values captured before the scenario touched them. | ||
| * | ||
| * Keys absent from Drupal state are stored with a sentinel marker so they | ||
| * can be deleted on revert rather than reset to NULL. | ||
| * | ||
| * @var array<string, array{exists: bool, value: mixed}> | ||
| */ | ||
| protected array $stateOriginalValues = []; | ||
|
|
||
| /** | ||
| * Reset the snapshot registry before each scenario. | ||
| */ | ||
| #[BeforeScenario] | ||
| public function stateBeforeScenario(BeforeScenarioScope $scope): void { | ||
| $this->stateOriginalValues = []; | ||
| } | ||
|
|
||
| /** | ||
| * Revert every touched state key after the scenario finishes. | ||
| */ | ||
| #[AfterScenario] | ||
| public function stateAfterScenario(AfterScenarioScope $scope): void { | ||
| if ( | ||
| $scope->getScenario()->hasTag('behat-steps-skip:' . __FUNCTION__) | ||
| || $scope->getScenario()->hasTag('behat-steps-skip:StateTrait') | ||
| ) { | ||
| $this->stateOriginalValues = []; | ||
| return; | ||
| } | ||
|
|
||
| $state = \Drupal::state(); | ||
| foreach ($this->stateOriginalValues as $name => $snapshot) { | ||
| if ($snapshot['exists']) { | ||
| $state->set($name, $snapshot['value']); | ||
| } | ||
| else { | ||
| $state->delete($name); | ||
| } | ||
| } | ||
|
|
||
| $this->stateOriginalValues = []; | ||
| } | ||
|
|
||
| /** | ||
| * Set a Drupal state value. | ||
| * | ||
| * @code | ||
| * Given the state "my_module.launched" has the value "1" | ||
| * @endcode | ||
| */ | ||
| #[Given('the state :name has the value :value')] | ||
| public function stateSet(string $name, string $value): void { | ||
| $this->stateStoreOriginalValue($name); | ||
| \Drupal::state()->set($name, $this->stateNormaliseValue($value)); | ||
| } | ||
|
|
||
| /** | ||
| * Delete a Drupal state value. | ||
| * | ||
| * @code | ||
| * Given the state "my_module.launched" does not exist | ||
| * @endcode | ||
| */ | ||
| #[Given('the state :name does not exist')] | ||
| public function stateDelete(string $name): void { | ||
| $this->stateStoreOriginalValue($name); | ||
| \Drupal::state()->delete($name); | ||
| } | ||
|
|
||
| /** | ||
| * Set multiple Drupal state values from a table. | ||
| * | ||
| * @code | ||
| * Given the following state values: | ||
| * | name | value | | ||
| * | my_module.launched | 1 | | ||
| * | my_module.feature_flag | 0 | | ||
| * @endcode | ||
| */ | ||
| #[Given('the following state values:')] | ||
| public function stateSetMultiple(TableNode $table): void { | ||
| $state = \Drupal::state(); | ||
| foreach ($table->getHash() as $row) { | ||
| if (!isset($row['name']) || !array_key_exists('value', $row)) { | ||
| throw new \RuntimeException('The state values table must contain "name" and "value" columns.'); | ||
| } | ||
| $name = $row['name']; | ||
| $this->stateStoreOriginalValue($name); | ||
| $state->set($name, $this->stateNormaliseValue($row['value'])); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Assert that a Drupal state value equals an expected value. | ||
| * | ||
| * @code | ||
| * Then the state "my_module.launched" should have the value "1" | ||
| * @endcode | ||
| */ | ||
| #[Then('the state :name should have the value :value')] | ||
| public function stateAssertHasValue(string $name, string $value): void { | ||
| $state_value = $this->stateReadValue($name); | ||
| if (!$state_value['exists']) { | ||
| throw new \Exception(sprintf('The state "%s" does not exist, but it should have the value "%s".', $name, $value)); | ||
| } | ||
|
|
||
| $expected = $this->stateNormaliseValue($value); | ||
| $actual_stringified = $this->stateStringifyValue($state_value['value']); | ||
| $expected_stringified = $this->stateStringifyValue($expected); | ||
| if ($actual_stringified !== $expected_stringified) { | ||
| throw new \Exception(sprintf('The state "%s" has the value "%s", but it should have the value "%s".', $name, $actual_stringified, $expected_stringified)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Assert that a Drupal state key does not exist. | ||
| * | ||
| * @code | ||
| * Then the state "my_module.launched" should not exist | ||
| * @endcode | ||
| */ | ||
| #[Then('the state :name should not exist')] | ||
| public function stateAssertNotExists(string $name): void { | ||
| $state_value = $this->stateReadValue($name); | ||
| if ($state_value['exists']) { | ||
| throw new \Exception(sprintf('The state "%s" exists with the value "%s", but it should not exist.', $name, $this->stateStringifyValue($state_value['value']))); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Read a state value, distinguishing stored NULL from a missing key. | ||
| * | ||
| * Uses the underlying key/value store's `has()` so that a legitimately | ||
| * stored NULL is reported as existing. `\Drupal::state()->get()` cannot | ||
| * distinguish the two cases because it applies the `??` operator to | ||
| * the loaded value and returns the default for NULL. | ||
| * | ||
| * @param string $name | ||
| * The state key name. | ||
| * | ||
| * @return array{exists: bool, value: mixed} | ||
| * An associative array with `exists` (bool) and `value` (mixed). | ||
| */ | ||
| protected function stateReadValue(string $name): array { | ||
| $key_value = \Drupal::keyValue('state'); | ||
|
|
||
| if (!$key_value->has($name)) { | ||
| return ['exists' => FALSE, 'value' => NULL]; | ||
| } | ||
|
|
||
| return ['exists' => TRUE, 'value' => \Drupal::state()->get($name)]; | ||
| } | ||
|
|
||
| /** | ||
| * Store the original state value for a key on first access. | ||
| * | ||
| * @param string $name | ||
| * The state key name. | ||
| */ | ||
| protected function stateStoreOriginalValue(string $name): void { | ||
| if (array_key_exists($name, $this->stateOriginalValues)) { | ||
| return; | ||
| } | ||
|
|
||
| $this->stateOriginalValues[$name] = $this->stateReadValue($name); | ||
| } | ||
|
|
||
| /** | ||
| * Normalise a string value from a step into the shape actually stored. | ||
| * | ||
| * @param string $value | ||
| * The raw value captured from the step or table cell. | ||
| * | ||
| * @return mixed | ||
| * The normalised value: decoded JSON for array/object input, integer or | ||
| * float for numeric input, boolean for "true"/"false", NULL for "null", | ||
| * or the original string otherwise. | ||
| */ | ||
| protected function stateNormaliseValue(string $value): mixed { | ||
| $trimmed = trim($value); | ||
|
|
||
| if ($trimmed === '') { | ||
| return $value; | ||
| } | ||
|
|
||
| $lower = strtolower($trimmed); | ||
| if ($lower === 'true') { | ||
| return TRUE; | ||
| } | ||
| if ($lower === 'false') { | ||
| return FALSE; | ||
| } | ||
| if ($lower === 'null') { | ||
| return NULL; | ||
| } | ||
|
|
||
| if ($trimmed[0] === '{' || $trimmed[0] === '[') { | ||
| $decoded = json_decode($trimmed); | ||
| if (json_last_error() === JSON_ERROR_NONE) { | ||
| return $decoded; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| if (is_numeric($trimmed)) { | ||
| return str_contains($trimmed, '.') ? (float) $trimmed : (int) $trimmed; | ||
| } | ||
|
Comment on lines
+233
to
+235
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
Sources: PHP “Numeric strings” manual (scientific notation rules) [1], 🏁 Script executed: # Examine the actual file around the specified lines
cd /tmp/repo 2>/dev/null || cd .
if [ -f "src/Drupal/StateTrait.php" ]; then
echo "=== Lines 225-245 from StateTrait.php ==="
sed -n '225,245p' src/Drupal/StateTrait.php
else
echo "File not found, searching for StateTrait.php..."
find . -name "StateTrait.php" -type f 2>/dev/null | head -5
fiRepository: drevops/behat-steps Length of output: 567 🏁 Script executed: # Also check the full method signature and context around lines 233-235
if [ -f "src/Drupal/StateTrait.php" ]; then
echo "=== Full stateNormaliseValue method ==="
rg -A 30 "function stateNormaliseValue" src/Drupal/StateTrait.php
fiRepository: drevops/behat-steps Length of output: 777 🏁 Script executed: # Search for any test coverage of numeric normalization
if [ -d "tests" ] || [ -d "test" ]; then
echo "=== Search for tests covering numeric normalization ==="
rg "stateNormaliseValue|1e3|scientific|exponent" tests/ 2>/dev/null || \
rg "stateNormaliseValue|1e3|scientific|exponent" test/ 2>/dev/null || \
echo "No tests found with those patterns"
fiRepository: drevops/behat-steps Length of output: 372 Handle scientific-notation numbers correctly during normalization. In Use 🤖 Prompt for AI Agents |
||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| /** | ||
| * Stringify a state value for comparison and error messages. | ||
| * | ||
| * @param mixed $value | ||
| * The value to stringify. | ||
| * | ||
| * @return string | ||
| * The stringified value. | ||
| */ | ||
| protected function stateStringifyValue(mixed $value): string { | ||
| if ($value === NULL) { | ||
| return 'NULL'; | ||
| } | ||
| if (is_bool($value)) { | ||
| return $value ? 'true' : 'false'; | ||
| } | ||
| if (is_scalar($value)) { | ||
| return (string) $value; | ||
| } | ||
| return (string) json_encode($value); | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.