Ejemplo
symfony/console 7.4.16: Handle unmanaged stream descriptors in StreamOutput, static stream caching in ConsoleOutput, unclosed style leaks in OutputFormatter, EOF interactivity muting in QuestionHelper, and stderr routing in ProgressBar
Muestra verificada para composer symfony/console 7.4.16: Handle unmanaged stream descriptors in StreamOutput, static stream caching in ConsoleOutput…
sha256:a9a9bbef69b2587102aa48a9ec43d167d4465479d098c5a66308fa41b25f49bb
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 2
- Claves de firma que lo compilaron
- 2
Entorno declarado
php linux x64 php php composer
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| php 8 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · composer@1 |
2026-08-16 |
| php 8 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · composer@1 |
2026-08-18 |
Caso
HOW- Objetivo
- Handle unmanaged stream descriptors in StreamOutput, static stream caching in ConsoleOutput, unclosed style leaks in OutputFormatter, EOF interactivity muting in QuestionHelper, and stderr routing in ProgressBar
- Paquetes
- Símbolos
-
- Symfony\Component\Console\Output\StreamOutput
- Symfony\Component\Console\Output\ConsoleOutput
- Symfony\Component\Console\Output\BufferedOutput
- Symfony\Component\Console\Formatter\OutputFormatter
- Symfony\Component\Console\Helper\ProgressBar
- Symfony\Component\Console\Helper\QuestionHelper
- Symfony\Component\Console\Cursor
- Entorno
- php
- Creado
- 2026-08-16T14:03:33Z
Contrato
- StreamOutput does not close its underlying stream resource when destroyed, leaving the file descriptor open unless explicitly closed with fclose by the caller
- ConsoleOutput caches its stdout and stderr streams in private static variables, sharing the identical stream resource across separate ConsoleOutput instances
- OutputFormatter retains unclosed tag styles on OutputFormatterStyleStack across separate format and write calls, propagating styles into subsequent messages until explicitly closed with </>
- QuestionHelper on encountering EOF from a StreamableInputInterface permanently sets isInteractive to false on the input instance, silently disabling interactive prompts for all subsequent questions
- ProgressBar instantiated with ConsoleOutputInterface writes progress output to getErrorOutput() (STDERR) instead of STDOUT, and ProgressBar::finish() omits a trailing newline
- BufferedOutput::fetch() destructively drains the internal buffer rather than caching it, returning an empty string on subsequent calls
- Cursor::hide() and Cursor::show() emit the ANSI sequences \x1b[?25l and \x1b[?25h\x1b[?0c without resetting cursor visibility upon command completion or object destruction
Archivos
- NOTES.md
- composer.json
- composer.lock
- csx.json
- test/contract.php
Código fuente
# Notes
## Search Results
`search_known_solution` returned candidate samples `sha256:14dd434489e1359c9763ff1d4b367d176e5e60de805649a7f09831dc5b572bc2` (Go SDK attribute merge), `sha256:19df5703d8dc81fc82d005c9a1da5e4e2cbf15e79f3bda3895e0373c83079fce` (OpenTelemetry slice truncation), and `sha256:e0ab881bb6e03a1d68177d32a6688b85505ae28037acb54405bac8d34cdf554c` (Go embed path formatting). This sample addresses a distinct angle: resource lifecycle, stream closure omissions in `StreamOutput`, process-wide stream caching in `ConsoleOutput`, style stack state retention across independent formatting calls in `OutputFormatter`, silent interactivity muting on EOF in `QuestionHelper`, and stderr stream routing in `ProgressBar`.
## What a Naive Model Would Write
A naive model expects that destroying a `StreamOutput` automatically closes its wrapped PHP stream resource, that instantiating `ConsoleOutput` opens separate, unshared stream descriptors, that `OutputFormatter` resets open ANSI style tags between separate `format()` calls, that `QuestionHelper` maintains stream interactivity across successive questions even after empty reads, and that `ProgressBar` writes progress feedback to standard output.
## How the Wrong Version Fails
Fails silently with leaked file descriptors when omitting explicit `fclose()`, cross-instance stream corruption if a cached `ConsoleOutput` handle is closed, persistent ANSI color code bleed into subsequent console output when tags are unclosed, silent suppression of interactive question prompts following an EOF read, and missing progress bar output when piping STDOUT.
{
"require": {
"symfony/console": "^7.4"
}
}
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "365120514db039d11645629eab431730",
"packages": [
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "symfony/console",
"version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d",
"reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/string": "^7.2|^8.0"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
"symfony/dotenv": "<6.4",
"symfony/event-dispatcher": "<6.4",
"symfony/lock": "<6.4",
"symfony/process": "<6.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/lock": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/stopwatch": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command-line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.4.16"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-07-31T12:37:14+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.7-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-06-05T06:23:12+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-07-28T08:25:59+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.38.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-25T13:48:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.38.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-27T06:59:30+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0",
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.7-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.7.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-06-16T09:55:08+00:00"
},
{
"name": "symfony/string",
"version": "v8.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc",
"reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc",
"shasum": ""
},
"require": {
"php": ">=8.4.1",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-intl-grapheme": "^1.33",
"symfony/polyfill-intl-normalizer": "^1.0",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
"symfony/emoji": "^7.4|^8.0",
"symfony/http-client": "^7.4|^8.0",
"symfony/intl": "^7.4|^8.0",
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.1.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-07-28T07:35:25+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
{"case":{"believed":"A model expects that destroying StreamOutput closes its underlying stream resource, ConsoleOutput opens fresh stream handles per instance, OutputFormatter resets style tags between format calls, and ProgressBar writes output to STDOUT.","caseId":"case:sha256:1baa5f4c60e78c8b1ff9bb4347c15c799ea9b2b841fcadff4332d0f3fc47556d","contract":["StreamOutput does not close its underlying stream resource when destroyed, leaving the file descriptor open unless explicitly closed with fclose by the caller","ConsoleOutput caches its stdout and stderr streams in private static variables, sharing the identical stream resource across separate ConsoleOutput instances","OutputFormatter retains unclosed tag styles on OutputFormatterStyleStack across separate format and write calls, propagating styles into subsequent messages until explicitly closed with \u003c/\u003e","QuestionHelper on encountering EOF from a StreamableInputInterface permanently sets isInteractive to false on the input instance, silently disabling interactive prompts for all subsequent questions","ProgressBar instantiated with ConsoleOutputInterface writes progress output to getErrorOutput() (STDERR) instead of STDOUT, and ProgressBar::finish() omits a trailing newline","BufferedOutput::fetch() destructively drains the internal buffer rather than caching it, returning an empty string on subsequent calls","Cursor::hide() and Cursor::show() emit the ANSI sequences \\x1b[?25l and \\x1b[?25h\\x1b[?0c without resetting cursor visibility upon command completion or object destruction"],"goal":"Handle unmanaged stream descriptors in StreamOutput, static stream caching in ConsoleOutput, unclosed style leaks in OutputFormatter, EOF interactivity muting in QuestionHelper, and stderr routing in ProgressBar","kind":"HOW","packages":["pkg:composer/symfony/console@7.4.16"],"schemaVersion":1,"symbols":["Symfony\\Component\\Console\\Output\\StreamOutput","Symfony\\Component\\Console\\Output\\ConsoleOutput","Symfony\\Component\\Console\\Output\\BufferedOutput","Symfony\\Component\\Console\\Formatter\\OutputFormatter","Symfony\\Component\\Console\\Helper\\ProgressBar","Symfony\\Component\\Console\\Helper\\QuestionHelper","Symfony\\Component\\Console\\Cursor"]},"contractCommand":["php","test/contract.php"],"environment":{"arch":"x64","ecosystem":"composer","executionContext":"php","language":"php","os":"linux","packageManager":"composer","runtime":"php","schemaVersion":1},"license":"MIT-0","packages":["pkg:composer/symfony/console@7.4.16"],"schemaVersion":1,"symbols":["Symfony\\Component\\Console\\Output\\StreamOutput","Symfony\\Component\\Console\\Output\\ConsoleOutput","Symfony\\Component\\Console\\Output\\BufferedOutput","Symfony\\Component\\Console\\Formatter\\OutputFormatter","Symfony\\Component\\Console\\Helper\\ProgressBar","Symfony\\Component\\Console\\Helper\\QuestionHelper","Symfony\\Component\\Console\\Cursor"],"verifierAdapter":"composer@1"}
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Question\Question;
// -----------------------------------------------------------------------------
// Contract 1: StreamOutput does not close its underlying stream resource when
// destroyed, leaving the file descriptor open unless explicitly closed with
// fclose by the caller
// -----------------------------------------------------------------------------
$streamHandle = fopen('php://memory', 'r+');
assert(is_resource($streamHandle), 'Precondition: streamHandle must be a valid resource');
$streamOutput = new StreamOutput($streamHandle);
$streamOutput->write('buffered data');
// Destroy the StreamOutput instance
unset($streamOutput);
assert(
is_resource($streamHandle),
'Contract 1 failed: StreamOutput destructor must leave the underlying stream open'
);
assert(
'stream' === get_resource_type($streamHandle),
'Contract 1 failed: Stream handle must retain active stream resource type'
);
// Explicit caller cleanup
fclose($streamHandle);
assert(
!is_resource($streamHandle),
'Contract 1 failed: Stream must be closed only after explicit fclose by caller'
);
// -----------------------------------------------------------------------------
// Contract 2: ConsoleOutput caches its stdout and stderr streams in private
// static variables, sharing the identical stream resource across separate
// ConsoleOutput instances
// -----------------------------------------------------------------------------
$consoleOutput1 = new ConsoleOutput();
$consoleOutput2 = new ConsoleOutput();
$stdoutResource1 = $consoleOutput1->getStream();
$stdoutResource2 = $consoleOutput2->getStream();
assert(
$stdoutResource1 === $stdoutResource2,
'Contract 2 failed: ConsoleOutput must reuse cached static stream resource across instances'
);
$stderrResource1 = $consoleOutput1->getErrorOutput()->getStream();
$stderrResource2 = $consoleOutput2->getErrorOutput()->getStream();
assert(
$stderrResource1 === $stderrResource2,
'Contract 2 failed: ConsoleOutput error stream must reuse cached static error resource across instances'
);
assert(
$stdoutResource1 !== $stderrResource1,
'Contract 2 failed: ConsoleOutput stdout and stderr must use distinct stream descriptors'
);
// -----------------------------------------------------------------------------
// Contract 3: OutputFormatter retains unclosed tag styles on OutputFormatterStyleStack
// across separate format and write calls, propagating styles into subsequent messages
// until explicitly closed with </>
// -----------------------------------------------------------------------------
$formatter = new OutputFormatter(true);
$formattedChunk1 = $formatter->format('<info>Unclosed step heading');
$formattedChunk2 = $formatter->format('Next line without tags');
// First chunk opens green style (\033[32m) and emits trailing reset
assert(
str_starts_with($formattedChunk1, "\033[32m"),
'Contract 3 failed: Chunk 1 must be styled with info green escape code'
);
// Second chunk inherits info style because style stack retained unclosed <info>
assert(
str_starts_with($formattedChunk2, "\033[32m"),
'Contract 3 failed: Chunk 2 must inherit unclosed info style from formatter style stack'
);
assert(
str_contains($formatter->getStyleStack()->getCurrent()->apply('test'), "\033[32m"),
'Contract 3 failed: Formatter style stack must retain active green style'
);
// Explicit closing tag resets stack
$formattedChunk3 = $formatter->format('</>Final plain text');
$formattedChunk4 = $formatter->format('Subsequent independent text');
assert(
!str_contains($formattedChunk4, "\033[32m"),
'Contract 3 failed: Chunk 4 must not have green styling after closing tag pops stack'
);
// -----------------------------------------------------------------------------
// Contract 4: QuestionHelper on encountering EOF from a StreamableInputInterface
// permanently sets isInteractive to false on the input instance, silently
// disabling interactive prompts for all subsequent questions
// -----------------------------------------------------------------------------
$inputStream = fopen('php://memory', 'r+');
fwrite($inputStream, "alice\n");
rewind($inputStream);
$streamInput = new ArrayInput([]);
$streamInput->setStream($inputStream);
$streamInput->setInteractive(true);
$questionHelper = new QuestionHelper();
$promptOutput = new BufferedOutput();
// First question successfully reads line from stream
$answer1 = $questionHelper->ask($streamInput, $promptOutput, new Question('Name: '));
assert(
$answer1 === 'alice',
'Contract 4 failed: First question must read alice from stream'
);
assert(
$streamInput->isInteractive() === true,
'Contract 4 failed: Input must remain interactive while stream has data'
);
// Second question encounters EOF; fallback default is returned but interactivity is permanently muted
$answer2 = $questionHelper->ask($streamInput, $promptOutput, new Question('City: ', 'default_city'));
assert(
$answer2 === 'default_city',
'Contract 4 failed: Second question must return default on EOF'
);
assert(
$streamInput->isInteractive() === false,
'Contract 4 failed: QuestionHelper must set isInteractive to false on input upon EOF'
);
// Even if more data is appended to the stream, subsequent ask() immediately returns default without reading
fwrite($inputStream, "bob\n");
$answer3 = $questionHelper->ask($streamInput, $promptOutput, new Question('Nick: ', 'default_nick'));
assert(
$answer3 === 'default_nick',
'Contract 4 failed: Non-interactive input must bypass stream and immediately return default answer'
);
fclose($inputStream);
// -----------------------------------------------------------------------------
// Contract 5: ProgressBar instantiated with ConsoleOutputInterface writes
// progress output to getErrorOutput() (STDERR) instead of STDOUT, and
// ProgressBar::finish() omits a trailing newline
// -----------------------------------------------------------------------------
$stdoutMem = fopen('php://memory', 'r+');
$stderrMem = fopen('php://memory', 'r+');
$customConsoleOutput = new ConsoleOutput();
$refStderr = new \ReflectionProperty(ConsoleOutput::class, 'stderr');
$refStderr->setValue($customConsoleOutput, new StreamOutput($stderrMem));
$refStdout = new \ReflectionProperty(StreamOutput::class, 'stream');
$refStdout->setValue($customConsoleOutput, $stdoutMem);
$progressBar = new ProgressBar($customConsoleOutput, 10);
$progressBar->start();
$progressBar->advance(5);
$progressBar->finish();
rewind($stdoutMem);
rewind($stderrMem);
$stdoutContent = stream_get_contents($stdoutMem);
$stderrContent = stream_get_contents($stderrMem);
assert(
$stdoutContent === '',
'Contract 5 failed: ProgressBar on ConsoleOutputInterface must not write to STDOUT'
);
assert(
$stderrContent !== '',
'Contract 5 failed: ProgressBar on ConsoleOutputInterface must write to STDERR'
);
assert(
!str_ends_with($stderrContent, "\n") && !str_ends_with($stderrContent, "\r\n"),
'Contract 5 failed: ProgressBar::finish() must not write a trailing newline'
);
fclose($stdoutMem);
fclose($stderrMem);
// -----------------------------------------------------------------------------
// Contract 6: BufferedOutput::fetch() destructively drains the internal buffer
// rather than caching it, returning an empty string on subsequent calls
// -----------------------------------------------------------------------------
$bufferedOutput = new BufferedOutput();
$bufferedOutput->writeln('persistent audit line');
$firstFetch = $bufferedOutput->fetch();
$secondFetch = $bufferedOutput->fetch();
assert(
str_starts_with($firstFetch, 'persistent audit line'),
'Contract 6 failed: First fetch must return buffered content'
);
assert(
$secondFetch === '',
'Contract 6 failed: Second fetch must return empty string because fetch drains buffer'
);
// -----------------------------------------------------------------------------
// Contract 7: Cursor::hide() and Cursor::show() emit the ANSI sequences
// \x1b[?25l and \x1b[?25h\x1b[?0c without resetting cursor visibility upon
// command completion or object destruction
// -----------------------------------------------------------------------------
$cursorBuffer = new BufferedOutput();
$cursor = new Cursor($cursorBuffer);
$cursor->hide();
$hideSequence = $cursorBuffer->fetch();
assert(
$hideSequence === "\x1b[?25l",
'Contract 7 failed: Cursor::hide() must emit \x1b[?25l'
);
// Destroy cursor without calling show() - no automatic reset is emitted
unset($cursor);
$unmanagedBuffer = $cursorBuffer->fetch();
assert(
$unmanagedBuffer === '',
'Contract 7 failed: Cursor destructor must not automatically emit cursor restore escape sequence'
);
// Explicit show() emits restore sequence
$restoredCursor = new Cursor($cursorBuffer);
$restoredCursor->show();
$showSequence = $cursorBuffer->fetch();
assert(
$showSequence === "\x1b[?25h\x1b[?0c",
'Contract 7 failed: Cursor::show() must emit \x1b[?25h\x1b[?0c'
);
echo "OK: All symfony/console resource handling contracts verified.\n";