Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
- php: '8.1'
symfony-versions: '^5.4'
coverage: 'none'
- description: 'Symfony 8 + Behat 4.x-dev'
php: '8.4'
symfony-versions: '8.0.*'
coverage: 'none'
- description: 'Log Code Coverage'
php: '8.4'
coverage: 'xdebug'
Expand Down Expand Up @@ -60,6 +64,10 @@ jobs:
key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer

- name: Use Behat 4.x-dev with Symfony 8
if: matrix.symfony-versions == '8.0.*'
run: composer require behat/behat:4.x-dev --no-update --no-scripts

- name: Update Symfony version
if: matrix.symfony-versions != ''
run: |
Expand All @@ -86,4 +94,22 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: './coverage.xml'
fail_ci_if_error: true
fail_ci_if_error: false

prefer-lowest:
name: PHPUnit (composer --prefer-lowest)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'

- name: Install lowest compatible dependencies
run: composer update --prefer-lowest --no-interaction --no-progress

- name: Run PHPUnit tests
run: composer phpunit
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Agents

## Cursor Cloud specific instructions

This is a PHP Composer package (Symfony Bundle) — not a standalone application. No databases, caches, or external services required.

### Quick reference

All dev commands are Composer scripts defined in `composer.json`:

| Command | Purpose |
|---|---|
| `composer run dev-checks` | Full suite: validate + phpstan + phpcs + phpunit |
| `composer run phpunit` | PHPUnit tests only (no coverage) |
| `composer run phpstan` | Static analysis (max level) |
| `composer run code-style` | PHP CodeSniffer lint |
| `composer run code-style-fix` | Auto-fix coding standard violations |

See `CONTRIBUTING.md` for full contribution guidelines.

### Gotchas

- The `phpcs.xml.dist` uses `ignore_errors_on_exit=1`, so `composer run code-style` returns exit code 0 even with errors. Check output text for `ERROR` lines.
- PHPUnit config (`phpunit.xml.dist`) uses some deprecated attributes for PHPUnit 10 — the deprecation warnings are expected and harmless.
- The repo's `docker-compose.yaml` targets PHP 8.0 (outdated vs the 8.1+ requirement). Use a local PHP 8.1+ install instead of the Docker setup.
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: cs-fix rector phpunit phpstan phpcs dev-checks

cs-fix:
composer run code-style-fix

rector:
@if [ -x vendor/bin/rector ]; then vendor/bin/rector --dry-run; else echo "rector not installed; skipping"; fi

phpunit:
composer run phpunit

phpstan:
composer run phpstan

phpcs:
composer run code-style

dev-checks:
composer run dev-checks
23 changes: 13 additions & 10 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,26 @@
}
],
"license": "MIT",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"ext-json": "*",
"php": "^8.1",
"behat/behat": "^3.0",
"symfony/config": "^5.4 || ^6.4 || ^7.0",
"symfony/dependency-injection": "^5.4.34 || ^6.4 || ^7.1",
"symfony/http-client": "^5.4 || ^6.4 || ^7.1",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.1",
"symfony/routing": "^5.4 || ^6.4 || ^7.1",
"behat/behat": "^3.0 || 4.x-dev@dev",
"symfony/config": "^5.4 || ^6.4 || ^7.0 || ^8.0",
"symfony/dependency-injection": "^5.4.34 || ^6.4 || ^7.1 || ^8.0",
"symfony/http-client": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"symfony/routing": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"macpaw/similar-arrays": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "^7.0",
"squizlabs/php_codesniffer": "^3.6",
"doctrine/orm": "^2.0"
"phpunit/phpunit": "^10.5 || ^11.0 || ^12.0",
"slevomat/coding-standard": "^8.0",
"squizlabs/php_codesniffer": "^4.0",
"doctrine/orm": "^2.0 || ^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0"
},
"autoload": {
"psr-4": {
Expand Down
14 changes: 13 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@

<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array" value="eval=>NULL,dd=>NULL,die=>NULL,var_dump=>NULL,dump=>NULL,sizeof=>count,delete=>unset,print=>echo,echo=>NULL,print_r=>NULL,create_function=>NULL"/>
<property name="forbiddenFunctions" type="array">
<element key="eval" value="null"/>
<element key="dd" value="null"/>
<element key="die" value="null"/>
<element key="var_dump" value="null"/>
<element key="dump" value="null"/>
<element key="sizeof" value="count"/>
<element key="delete" value="unset"/>
<element key="print" value="echo"/>
<element key="echo" value="null"/>
<element key="print_r" value="null"/>
<element key="create_function" value="null"/>
</property>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.FunctionSpacing">
Expand Down
13 changes: 2 additions & 11 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,5 @@ parameters:
- src
level: max
checkExplicitMixed: false
ignoreErrors:
-
message: '#Parameter \#1 \$json of function json_decode expects string, string\|false given.*#'
count: 3
path: ./src/Context/ApiContext.php
-
message: '#Parameter \#3 \$actualJSON of method BehatApiContext\\Context\\ApiContext::compareStructureResponse\(\) expects string, string\|false given.*#'
count: 1
path: ./src/Context/ApiContext.php
-
identifier: missingType.iterableValue
# Aligns Symfony Config generics with runtime checks (e.g. ArrayNodeDefinition) across Symfony versions.
treatPhpDocTypesAsCertain: false
13 changes: 6 additions & 7 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
backupStaticProperties="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
stopOnFailure="false"
failOnPhpunitWarning="false">
<testsuites>
<testsuite name="Behat Api Context Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<source>
<include>
<directory>./src</directory>
</include>
</source>
<coverage>
<report>
<clover outputFile="clover.xml"/>
</report>
Expand Down
31 changes: 25 additions & 6 deletions src/Context/ApiContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,11 @@ public function responseStatusCodeShouldBe(string $httpStatus): void
public function responseIsJson(): void
{
$response = $this->getResponse();
$data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
$body = $this->getResponseBody($response);
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

if (empty($data)) {
throw new RuntimeException("Response was not JSON\n" . $response->getContent());
throw new RuntimeException("Response was not JSON\n" . $body);
}
}

Expand All @@ -258,7 +259,7 @@ public function responseIsJson(): void
*/
public function responseEmpty(): void
{
if (!empty($this->getResponse()->getContent())) {
if ($this->getResponseBody($this->getResponse()) !== '') {
throw new RuntimeException('Content not empty');
}
}
Expand All @@ -271,7 +272,7 @@ public function responseEmpty(): void
public function responseShouldBeJson(PyStringNode $string): void
{
$expectedResponse = json_decode(trim($string->getRaw()), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponseBody($this->getResponse()), true, 512, JSON_THROW_ON_ERROR);

if ($expectedResponse !== $actualResponse) {
$prettyJSON = json_encode($actualResponse, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT, 512);
Expand All @@ -286,7 +287,7 @@ public function responseShouldBeJson(PyStringNode $string): void
*/
public function iGetParamFromJsonResponse(string $paramPath, string $valueKey): void
{
$actualResponse = json_decode($this->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponseBody($this->getResponse()), true, 512, JSON_THROW_ON_ERROR);
$pathKeys = explode('.', $paramPath);

foreach ($pathKeys as $key) {
Expand All @@ -307,7 +308,7 @@ public function iGetParamFromJsonResponse(string $paramPath, string $valueKey):
*/
public function responseShouldBeJsonWithVariableFields(string $variableFields, PyStringNode $string): void
{
$this->compareStructureResponse($variableFields, $string, $this->getResponse()->getContent());
$this->compareStructureResponse($variableFields, $string, $this->getResponseBody($this->getResponse()));
}

protected function compareStructureResponse(
Expand Down Expand Up @@ -388,6 +389,11 @@ protected function checkResponseHeader(string $headerName, string $headerValue):
}
}

/**
* @param array<string, mixed> $requestParams
*
* @return array<string, mixed>
*/
protected function convertRunnableCodeParams(array $requestParams): array
{
foreach ($requestParams as $key => $value) {
Expand Down Expand Up @@ -449,8 +455,21 @@ protected function getResponse(): Response
return $this->response;
}

/**
* @return array<string, mixed>
*/
public function geRequestParams(): array
{
return $this->requestParams;
}

private function getResponseBody(Response $response): string
{
$content = $response->getContent();
if ($content === false) {
throw new RuntimeException('The response body is not available.');
}

return $content;
}
}
13 changes: 8 additions & 5 deletions src/DependencyInjection/BehatApiContextExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

class BehatApiContextExtension extends Extension
{
/**
* @param array<string, mixed> $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
/** @var array<string, mixed> $config */
$config = $this->processConfiguration($configuration, $configs);

$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));

$this->loadApiContext($config, $loader, $container);
}
Expand All @@ -28,10 +31,10 @@ public function load(array $configs, ContainerBuilder $container): void
*/
private function loadApiContext(
array $config,
XmlFileLoader $loader,
PhpFileLoader $loader,
ContainerBuilder $container
): void {
$this->safeLoad($loader, 'api_context.xml');
$this->safeLoad($loader, 'api_context.php');

$this->configureKernelResetManagers(
$config,
Expand Down Expand Up @@ -62,7 +65,7 @@ private function configureKernelResetManagers(
}
}

private function safeLoad(XmlFileLoader $loader, string $file): void
private function safeLoad(PhpFileLoader $loader, string $file): void
{
$loader->load($file);
}
Expand Down
21 changes: 15 additions & 6 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace BehatApiContext\DependencyInjection;

use LogicException;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand All @@ -13,7 +15,15 @@ class Configuration implements ConfigurationInterface
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('behat_api_context');
$root = $treeBuilder->getRootNode()->children();
$rootNode = $treeBuilder->getRootNode();
// Symfony TreeBuilder root is always ArrayNodeDefinition; kept for static analysis / defensive parity.
// @codeCoverageIgnoreStart
if (!$rootNode instanceof ArrayNodeDefinition) {
throw new LogicException('Expected configuration root to be an array node.');
}
// @codeCoverageIgnoreEnd

$root = $rootNode->children();

$this->addKernelResetManagersSection($root);

Expand All @@ -22,10 +32,9 @@ public function getConfigTreeBuilder(): TreeBuilder

private function addKernelResetManagersSection(NodeBuilder $builder): void
{
$builder
->arrayNode('kernel_reset_managers')
->scalarPrototype()->end()
->end()
->end();
$kernelResetManagers = $builder->arrayNode('kernel_reset_managers');
$kernelResetManagers->scalarPrototype()->end();
$kernelResetManagers->end();
$builder->end();
}
}
20 changes: 20 additions & 0 deletions src/Resources/config/api_context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

use BehatApiContext\Context\ApiContext;
use BehatApiContext\Service\ResetManager\DoctrineResetManager;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
$services = $container->services();

$services->set(ApiContext::class)
->public()
->autowire()
->autoconfigure();

$services->set(DoctrineResetManager::class)
->autowire(false)
->autoconfigure(false);
};
10 changes: 0 additions & 10 deletions src/Resources/config/api_context.xml

This file was deleted.

Loading
Loading